Chapter 12: IO with Serialization

12.1: Serializing Objects

Class java.io.ObjectOutputStream and java.io.ObjectInputStream use the same protocol to serialize and deserialize objects. When ObjectOutputStream outputs an object, it serializes the object into standard data stream which is understandable by underlying streams such as FileOutputStream or ByteArrayOutputStream. When an java.io.ObjectInputStream reads that stream, it understands it and can deserialize the stream back to an object.

* Output an object

Call ObjectOutputStream's method writeObject(Object). If you output a class which is not made serializable, a NotSerializableException will be thrown.

* Input an object

Call ObjectInputStream's method readObject( ) which returns an Object handle, cast this handle to your expected type. If it is not the expected type, a ClassCastException is thrown. The objects must be read from the stream in the same order in which they were written.

* Make a class serializable

To make your class serializable, simply let it implement interface java.io.Serializable. This interface is totally empty. It just acts as a flag to indicate that this class is serializable.

The serialization is handled by the ObjectOutputStream's method defaultWriteObject. This method writes everything which is needed to reconstruct an instance of the class, including the class's composition objects.

For many classes, this default behavior is good enough. However, default serialization can be slow, and a class might want more explicit control over the serialization. Refer to Java document for details.

   
   import java.io.*;
   
   class Employee implements Serializable{
      private String name;
      private char gender;
      private int age;
   
      public Employee(String n, char g, int a)
      {
         name = n;
         gender = g;
         age = a;
      }
   
      public String toString()
      {
         return "\n Employee name: " + name + "\n Gender: " + gender +
                "\n Age: " + age + "\n";
      }
   }
   
   class Dog implements Serializable{
      private String name;
      private String color;
      private int age;
   
      public Dog(String n, String c, int a)
      {
         name = n;
         color = c;
         age = a;
      }
   
      public String toString()
      {
         return "\n Dog name: " + name + "\n Color: " + color + 
                "\n Age: " + age + "\n";
      }
   }
   
   public class Test {
      public static void main(String [] args)
      {
         try{
            ObjectOutputStream os = new ObjectOutputStream(
                                    new BufferedOutputStream(
                                    new FileOutputStream("Employee")));
            Employee e1 = new Employee("Frank", 'M', 33);
            Employee e2 = new Employee("Ellen", 'F', 31);
            Dog d = new Dog("Snoopy", "Light yellow", 1);
            os. writeObject(e1);
            os. writeObject(e2);
            os. writeObject(d);
            os. flush();
            os. close();
   
            ObjectInputStream is = new ObjectInputStream(
                                   new FileInputStream("Employee"));
            Employee ee1 = (Employee)is. readObject();
            Employee ee2 = (Employee)is. readObject();
            Dog dd1 = (Dog)is. readObject();
            System.out.println("Object read from file: \n \n" + ee1 + ee2 + dd1);
            is. close();
         }
         catch(ClassNotFoundException e)
         {  System.out.println("ClassNotFoundException!"); }
         catch(ClassCastException e)
         {  System.out.println("ClassCastException!"); }
         catch(NotSerializableException e)
         {  System.out.println("NotSerializableException!"); }
         catch(IOException e)
         {  System.out.println("IOException!"); }
      }

Output will be:

   
   Three object read from file are:
   Employee name: Frank
   Gender: M
   Age: 33
   Employee name: Ellen
   Gender: F
   Age: 31
   Dog name: Snoopy
   Color: Light yellow
   Age: 1

ObjectOutputStream implements the DataOutput interface that defines many methods for writing primitive data types, such as writeInt, writeFloat, or writeUTF. You can use these methods to write primitive data types to an ObjectOutputStream. So it is with ObectInputStream.

When ObjectInputStream deserializes the objects, it does not call the constructor of the object. All information needed for restoring the object is read in from the InputStream.

* The .class file is needed to restore an unknown object

The serialized output does not contain the class definition. Only under the condition that the Java Virtual Machine knows the class definition, can it deserialize an object. For example, if we put serialized output file "Employee" in last example into a new directory, and use the following code to read it:

   
   import java.io.*;
   public class Test {
      public static void main(String [] args)
      {
         try{
            ObjectInputStream is = new ObjectInputStream(
                                   new FileInputStream("Employee"));
            Object ee1 = is. readObject();
            Object ee2 = is. readObject();
            Object dd1 = is. readObject();
            System.out.println("Object read from file: \n \n" + ee1 + ee2 + dd1);
            is. close();
         }
         catch(ClassNotFoundException e)
         {  System.out.println("ClassNotFoundException!"); }
         catch(IOException e)
         {  System.out.println("IOException!"); }
      }

ClassNotFoundException will be thrown.

12.2: Fully Customize Serialization with Externalizable

In most cases the default serialization behavior is enough. But this process may be slow, and for security reasons you may not want to serialize part of your object (e.g. password), or it is just not necessary.

To inform ObjectOutputStream and ObjectInputStream that you want to do it yourself, just implement interface java.io.Externalizable instead of Serializable, then implement its two methods:

   
   public void writeExternal(ObjectOutput) throws IOException
   public void readExternal(ObjectInput) throws IOException, ClassNotFoundException

When ObjectOutputStream sees that the object to be serialized implements interface Externalizable, it will create an ObjectOutput object from the output stream and pass it to that object's writeExternal method. That method will then write all the content of the object in its own way.

When ObjectInputStream sees the above serialization result, it derives from it the class name of the object to be deserialized, loads in that class, finds out that it implements interface Externalizable. Then it will call that class's default constructor to create a default object, create an ObjectInput object from the input stream, pass it to the default object's readExternal method. Then this method will read in and initialize the default object in its own way.

So you can see that ObjectOutputStream and ObjectInputStream's role here is only encode and decode the name of the class, create an ObjectInput and ObjectOutput from the stream, and call the object's relevant methods. All serialization and deserialization work is done by the object itself.

Example:

   
   import java.io.*;
   
   class Employee implements Externalizable{
      private String name = "Unknown";
      private char gender = 'M';
      private int age = 0;
   
      public Employee()
      {  System.out.println("Employee default constructor!"); }
   
      public Employee(String n, char g, int a)
      {
         name = n;
         gender = g;
         age = a;
      }
   
      public String toString()
      {
         return "\n Employee name: " + name + "\n Gender: " + gender +
                "\n Age: " + age + "\n";
      }
   
      public void writeExternal(ObjectOutput out)
         throws IOException
      {
         out. writeObject(name);
         out. writeChar(gender);
         out. writeInt(age);
      }
   
      public void readExternal(ObjectInput in)
         throws IOException, ClassNotFoundException
      {
         name = (String)in. readObject();
         gender = in. readChar();
         age = in. readInt();
      }
   }
   
   class Dog implements Externalizable{
      private String name = "Unknown";
      private String color = "Unknown";
      private int age = 0;
   
      public Dog()
      {  System.out.println("Dog default constructor!"); }
   
      public Dog(String n, String c, int a)
      {
         name = n;
         color = c;
         age = a;
      }
   
      public String toString()
      {
         return "\n Dog name: " + name + "\n Color: " + color +
                "\n Age: " + age + "\n";
      }
   
      public void writeExternal(ObjectOutput out)
         throws IOException
      {
         out. writeObject(name);
         out. writeObject(color);
         out. writeInt(age);
      }
   
      public void readExternal(ObjectInput in)
         throws IOException, ClassNotFoundException
      {
         name = (String)in. readObject();
         color = (String)in. readObject();
         age = in. readInt();
      }
   
   }
   

The main( ) remain unchanged. Output will be:

   
   Employee default constructor!
   Employee default constructor!
   Dog default constructor!
   
   Object read from file:
   
    Employee name: Frank
    Gender: M
    Age: 33
   
    Employee name: Ellen
    Gender: F
    Age: 31
   
    Dog name: Snoopy
    Color: Light yellow
    Age: 1
   

Unlike the default serialization behavior which recovers the data fully from the stream and does not call any constructor, controlled serialization with Externalizable first calls the object's default constructor, than initialize the fields with data read from the stream. Therefore, Externalizable objects must have a public default constructor. Otherwise IOException will be thrown.

When you inherit from an Externalizable object, you may very probably need to call the base class's writeExternal and readExternal from the derived-class writeExternal and readExternal.

12.3: Mask Fields from Serialization with Keyword transient

After serialized, even private data in an object can be accessed by reading a file or intercepting a network transmission. Therefore, you may wish serialization to skip any field of the object which is sensitive such as password.

You can of course use Externalizable and only serialize the fields you want to. An easier way is to put keyword transient in front of a field which you do not want to serialize. Then that field is not written into the stream, and when recovering objects from the stream Java will not attempt to recover that field either:

   
   class Dog implements Serializable{
      private String name;
      transient private String color;
      private int age;
   
      public Dog(String n, String c, int a)
      {
         name = n;
         color = c;
         age = a;
      }
   
      public String toString()
      {
         return "\n Dog name: " + name + "\n Color: " + color +
                "\n Age: " + age + "\n";
      }
   }

Output will be:

   
   Object read from file:
   
    Employee name: Frank
    Gender: M
    Age: 33
   
    Employee name: Ellen
    Gender: F
    Age: 31
   
    Dog name: Snoopy
    Color: null
    Age: 1

12.4: Alternative to Externalizable

The alternative to Externalizable is: you implement Serializable, and implement the two following methods:

   
   private void writeObject(ObjectOutputStream) throws IOException
   
   private void readObject(ObjectInputStream) 
      throws IOException, ClassNotFoundException

When Java is serializing an object, it will first check whether this object has these two methods. If yes, it will call them. If no, it will call method defaultWriteObject( ) of ObjectOutputStream and defaultReadObject( ) of ObjectInputStream. Even if you decide to implement the two methods yourself, you can still call the two default methods in your own methods. If you do this, make sure that you call them first thing in your method.

12.5: Do All of the Serialization at One Time

As long as you serialize a whole lot of things together into one stream at one time, you can always safely recover them. However, if you serialize part of them into one stream, and the rest into another stream, or you do part of the objects at one time, go away and do something else, then come back and serialize the rest, you are risking the integrity of the serialization. The easiest and safest way is to put all objects into one collection (such as a Vector), then write that it into one stream with writeObject( ).

12.6: Static Fields Are Not Automatically Serialized

When you use Serializable to serialize objects, static fields are not serialized. You have to write them into the stream yourself - through writeExternal, writeObject as discussed before, or do it totally manually.