Serialization

Using a DataOutputStream, you could write an app that saves the data content of your objects as simple types. However, Java provides an even more powerful mechanism called object serialization that does almost all the work for you. In its simplest form, object serialization is an automatic way to save and load the state of an object. However, object serialization has depths that we cannot plumb within the scope of this tutorial, including complete control over the serialization process and interesting conundrums such as class versioning. Basically, an instance of any class that implements the Serializable interface can be saved to and restored from a stream. Special stream subclasses, ObjectInputStream and ObjectOutputStream, are used to serialize primitive types and objects. Subclasses of Serializable classes are also serializable. The default serialization mechanism saves the value of all of the object's fields, except those that are static and those marked transient. One of the most important (and tricky) things about serialization is that when an object is serialized, any object references it contains are also serialized. Serialization can capture entire "graphs" of interconnected objects and put them back together on the receiving end (we'll demonstrate this in an upcoming example). The implication is that any object we serialize must contain only references to other Serializable objects. We can take control by marking nonserializable variables as TRansient or overriding the default serialization mechanisms. The transient modifier can be applied to any instance variable to indicate that its contents are not useful outside of the current context and should never be saved. In the following example, we create a Hashtable and write it to a disk file called hash.ser. The Hashtable object is already serializable because it implements the Serializable interface.

 //file: Save.java
 import java.io.*;
 import java.util.*;
 public class Save {
 public static void main(String[] args) {
 Hashtable hash = new Hashtable( );
 hash.put("string", "Gabriel Garcia Marquez");
 hash.put("int", new Integer(26));
 hash.put("double", new Double(Math.PI));
 try {
 FileOutputStream fileOut = new FileOutputStream( "hash.ser" );
 ObjectOutputStream out = new
 ObjectOutputStream( fileOut );
 out.writeObject( hash );
 out.close( );
 }
 catch (Exception e) {
 System.out.println(e);
 }
 }
 }


First, we construct a Hashtable with a few elements in it. Then, in the lines of code inside the try block, we write the Hashtable to a file called hash.ser, using the writeObject( ) method of ObjectOutputStream. The ObjectOutputStream class is a lot like the DataOutputStream class, except that it includes the powerful writeObject( ) method. The Hashtable we created has internal references to the items it contains. Thus, these components are automatically serialized along with the Hashtable. We'll see this in the next example when we deserialize the Hashtable.

 //file: Load.java
 import java.io.*;
 import java.util.*;
 public class Load {
 public static void main(String[] args) {
 try {
 FileInputStream fileIn = new FileInputStream("hash.ser");
 ObjectInputStream in = new ObjectInputStream(fileIn);
 Hashtable hash = (Hashtable)in.readObject( );
 System.out.println( hash.toString( ) );
 }
 catch (Exception e) {
 System.out.println(e);
 }
 }
 }


In this example, we read the Hashtable from the hash.ser file, using the readObject( ) method of ObjectInputStream. The ObjectInputStream class is a lot like DataInputStream, except that it includes the readObject( ) method. The return type of readObject( ) is Object, so we need to cast it to a Hashtable. Finally, we print the contents of the Hashtable using its toString( ) method.

Initialization with readObject( )

Often simple deserialization alone is not enough to reconstruct the full state of an object. For example, the object may have had transient fields representing state that could not be serialized, such as network connections, event registration, or decoded image data. Objects have an opportunity to do their own setup after deserialization by implementing a special method named readObject( ). Not to be confused with the readObject( ) method of the ObjectInputStream, this method is implemented by the serializable object itself. The readObject( ) method must have a specific signature, and it must be private. The following snippet is taken from an animated JavaBean that we'll talk about in :

 private void readObject(ObjectInputStream s)
 throws IOException, ClassNotFoundException
 {
 s.defaultReadObject( );
 initialize( );
 if ( isRunning )
 start( );
 }


When the readObject( ) method with this signature exists in an object, it is called during the deserialization process. The argument to the method is the ObjectInputStream doing the object construction. We delegate to its defaultReadObject( ) method to do the normal deserialization and then do our custom setup. In this case, we call one of our methods, named initialize( ), and optionally, a method called start( ). We'll talk more about serialization in when we discuss JavaBeans, and we'll see that it is possible to serialize a graphical GUI component currently in use and bring it back to life later.

SerialVersionUID

Java object serialization was designed to accommodate certain kinds of compatible class changes or evolution in the structure of classes. For example, changing the methods of a class does not necessarily mean that its serialized representation has to change since only the data of variables is stored. Nor would simply adding a new field to a class necessarily prohibit us from loading an old serialized version of the class. We could simply allow the new variable to take its default value. By default, however, Java is very picky and errs on the side of caution. If you make any kind of change to the structure of your class, by default you'll get an InvalidClassException when trying to read previously serialized forms of the class. Java detects these versions by performing a hash function on the structure of the class and storing a 64-bit value, called the Serial Version UID (SUID), along with the serialized data. It can then compare the hash to the class when it is loaded. Java allows us to take control of this process by looking for a special, magic field in our classes that looks like the following:

 static final long serialVersionUID = -6849794470754667710L;


(The value is, of course, different for every class.) If it finds this static serialVersionUID long field in the class, it uses its value instead of performing the hash on the class. This value will be written out with serialized versions of the class and used for comparison when they are deserialized. This means that we are now in control of what versions of the class are compatible with what serialized representations. For example, we can create our serializable class from the beginning with our own SUID and then only increment it if we want to prevent older forms of the class from being loaded:

 class MyDataObject implements Serializable {
 static final long serialVersionUID = 1; // Version 1
 ...
 }


A utility called serialver that comes with the JDK allows you to calculate the hash that Java would use for the class yourself. This is necessary if you already have serialized objects stored and need to modify the class afterward. Running the serialver command on the class displays the SUID necessary to match the value already stored:

 % serialver SomeObject
  
 static final long serialVersionUID = -6849794470754667710L;


By placing this value into your class, you can "freeze" the SUID at the current value, allowing the class to change without being reloaded.

Java ScreenShot
Comments