Serializing JavaBeans

Serializing an object is the process of turning its state into a sequence of bytes that is neither a .java nor a .class file. Why would you want to do such a thing? That sequence of bytes can be saved as a file on a disk or sent over a network. When a request is made to restore an object from a file or on the other end of a network connection, the sequence of bytes is deserialized into its original structure.

For JavaBeans, serialization provides a simple way to save the state of a bean between one session and another. This is also called component persistence. For example, suppose the user of a bean changes the values of several properties. If the bean is serialized to a file and then deserializing the next time the user uses it, the bean is restored exactly as the user left it.

When you serialize a bean, you create a file that has the name of the bean class with a .ser file extension. For example, a serialized bean named MyBean would have the file name of MyBean.ser.

The following topics explain how to serialize and deserialize a JavaBean:

Serializing a bean in the UI Designer
Explains how to serialize a component using the UI Designer.

Instantiating serializable JavaBeans
Describes how to get JBuilder to instantiate a bean using the Beans.instantiate() method and explains why this is desirable.

Serializing an object with code
Describes how to serialize an object using ObjectOutputStream.

Deserializing an object
Describes how to restore an object using ObjectInputStream.

Customizing serialization and deserialization
Explains how to control serialization and deserialization by writing your own routines.

Serializing with the Externalizable interface
Briefly describes how to achieve complete control over the serialization and deserialization processes.

Versioning
Explains how to maintain serialization compatibility when a class definition changes slightly.


Serializing a bean in the UI Designer

The simplest way to serialize a JavaBean is to use the UI Designer. Modify the bean using the Component Inspector so it has the settings you want to keep. Then follow these steps:

  1. Select the component you want to serialize in the UI Designer.

  2. Right-click the component to display a menu.

  3. Choose Serialize Component on the menu.

  4. In the dialog box that appears, specify the name of the serialized component and the directory where you want the component saved.

    The dialog box suggests a file name, which is the name of the component with a .ser extension. Keep this name. Specify a directory that is on your class path.

  5. Choose OK.

The component is serialized to a file with the name you specified with a .ser file extension. The next time you instantiate that bean using Beans.instantiate(), the .ser file is read into memory.

Instantiating serializable JavaBeans

Usually you instantiate a Java object using the new keyword. When you want to create a JavaBean that is serializable, you should use the Beans.instantiate() method instead. Using Beans.instantiate() is preferrable because Beans.instantiate automatically loads any serialized version of the JavaBean (the name of the bean with a .ser file extension). You can modify the base class of your JavaBean and the change will be in effect the next time your bean is read back into memory (deserialized).

Beans.instantiate() also performs additional housekeeping tasks that applets need.

You can customize JBuilder so that the code it generates uses Beans.instantiate() instead of new:

  1. Choose File|Project Properties.
  2. Click the Code Style tab.
  3. Check the Use Beans.instantiate option.

Now JBuilder generates code that uses the Beans.instantiate() method to instantiate a JavaBean whenever the bean is dropped on the UI Designer.

To make Beans.instantiate() the default way JBuilder instantiates a bean,

  1. Choose Tools|Default Project Properties.
  2. Click the Code Style tab.
  3. Check the Use Beans.instantiate option.

Beans.instantiate() can throw an exception, so it must exist within the jbInit() method where exceptions are expected and handled. Beans are still declared where they were before, but their initializers appear inside jbInit().

For example, here is how code might appear using the new keyword to instantiate an object:

public class ButtonBoxFrame extends JFrame {
  XYLayout xYLayout = new borland.jbcl.layout.XYLayout();
  ButtonControl buttonControl1 = new ButtonControl;
  ButtonControl buttonControl1 = new ButtonControl;
  ButtonControl buttonControl1 = new ButtonControl;

public ButtonBoxFrame() {
  try
    jbInit();
  }
  catch (Exception e) {
    e.printStackTrace();
  }
}

private void jbInit() throws Exception{
  ...
}

This is how the code appears using Beans.instantiate():
public class ButtonBoxFrame extends JFrame {
  XYLayout xYLayout1;
  ButtonControl buttonControl1;
  ButtonControl buttonControl2;
  ButtonControl buttonControl3;
  
  public ButtonBoxFrame() {
    try {
	  jbInit();
	)
	catch (Exception e) {
	  e.printStackTrace();
	}
}

private void jbInit() throws Exception {
  xYLayout1 = (XYLayout)
    Beans.instantiate(getClass().getClassLoader(),
    XYLayout.class.getName());
  buttonControl1 = (ButtonControl) Beans.instantiate(getClass().getClassLoader(),
    ButtonControl.class.getName());
  buttonControl2 = (ButtonControl) Beans.instantiate(getClass().getClassLoader(),
    ButtonControl.class.getName());
  buttonControl3 = (ButtonControl) Beans.instantiate(getClass().getClassLoader(),
    ButtonControl.class.getName());
  ...
}

Serializing an object with code

A bean, or any other object, is serializable if it implements the Serializable interface. Serializable has no methods to implement so it is called a tagging or marking interface; its presence in the classes' implements statement indicates serialization can occur.

All of components on the Component Palette implement the Serializable interface.

If you want to control the serialization process yourself in the components you write, or if you want to serialize components that don't extend JBuilder components, you must understand the details of serialization and deserialization. The rest of this chapter is for you.

To serialize an object that implements Serializable, pass the object to the writeObject() method of an ObjectOutputStream. writeObject() writes out all the values of all the fields in the object, including those that are inherited.

Objects can contain other objects, which in turn can contain other objects. Java serialization can handle objects nested within objects automatically. For primitive fields in the object, writeObject() writes the values directly to the stream. When an object field is encountered, however, writeObject() is called again, and if that object contains another object, writeObject() continues to be called recursively until all the objects are written to the stream. All the programmer need do is pass the object to the writeObject() method of ObjectOutputStream and the rest happens automatically.

The following example creates a PersonalData object called mine. The code writes a string and the mine object to a stream, storing them in a file:

  public class PersonalData implements Serializable {
    public int   id
    public int   yearOfBirth;
    public float yearlySalary;
  }
  ...
 
  PersonalData mine = new PersonalData(101, 1956, 46500.00);
  FileOutputStream outstream = new FileOutputStream("PersonalData.ser");
  ObjectOutputStream out = new ObjectOutputStream(outstream);
  out.writeObject("My personal data");        // writes a string to the stream
  out.writeObject(mine);                      // writes the object to the stream
  out.close();                                // flushes and closes the stream
  ...

A FileOutputStream object is created and passed to an ObjectOutputStream. When out.writeObject() is called, the string and the mine objects are serialized into a sequence of bytes stored in the PersonalData.ser file.


Deserializing an object

To deserialize an object, call the readObject() method of an ObjectInputStream. The object is transformed from a series of bytes into its original structure. If the object contains other objects, the readObject() method is called recursively until all objects contained within are reconstructed into their original form.

The following code creates a FileInputStream object and passes it to an ObjectInputStream. Calling readObject() reads the string and the mine object from the file and restores them to their original form.

  FileInputStream instream = new FileInputStream("PersonalData.ser");
  ObjectInputStream in = new ObjectInputStream(instream);
  String s = (String)in.readObject();                   // reads the string from the stream
  PersonalData mine = (PersonalData)in.readObject();    // deserializes the bean from the stream

Note that the ObjectInputStream is downcast when the readObject() call is made. Whenever you call readObject() and assign the result to a variable, you must downcast the resulting object.


Customizing serialization

So far you've seen how to serialize objects to a stream and deserialize them back into their original form. If you implement the Serializable interface, your bean can be written to a stream and read from a stream with little effort.

Occasionally you might want more control over the serialization and deserialization process. This section explains how you can attain that goal.

Implementing the Serializable interface

As mentioned previously, a bean can be serialized if it implements the Serializable interface. Serializable exists in the java.io package, so import that package and specify that the bean you are creating implements this interface. Because Serializable has no methods to implement, you are done with this step once you've added Serializable to the implements statement.

Preventing specific fields from being serialized

Sometimes there are fields in a bean that you don't want serialized. Such a field could hold temporary information that wouldn't be valid the next time the bean is used or it could hold information that you don't want serialized for security reasons, such as a password.

To prevent a field from being serialized, add the transient keyword to the field's declaration. For example,

class wizzyComponent implements Serializable {
  private int height;
  private int width;
  private transient int mousePosX;    // this field will not be serialized
  private transient int mousePosY;    // this field will not be serialized
  ...
}

Now when wizzyComponent is serialized, the mousePosX and mousePosY variables are excluded. To the resulting, serialized object, it's as if these two variables don't exist.

Adding the writeObject() and readObject() methods

You can partially control how serialization and deserialization occurs by providing your own writeObject() and readObject() methods. The code within them writes and reads the object to and from a stream in the manner you specify instead of using the default serialization mechanism. This approach provides considerable control over how an object is serialized and deserialized.

To use this approach, add writeObject() and readObject() methods to your class. Their signatures must appear exactly like this:

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

The signatures of these methods are unusual because they are always declared as private, yet they are never called from the class in which they are declared. Also, they are not part of any interface, yet you must remember to copy the signatures exactly when you add the methods to your component.

Defining writeObject()

Within the writeObject() method, write the code that serializes your object to a stream.

Within the method you can activate the default serialization mechanism at any time by calling the defaultWriteObject() method of the ObjectOutputStream that is the argument of this writeObject() method. defaultWriteObject() serializes all the fields that are not marked as transient. You can perform additional processing before or after the call to defaultWriteObject(). For example, here is the writeObject() method of JBCL's ListCore component; it calls defaultWriteObject() immediately, but then creates a hash table and writes that hash table to the stream also:

private void writeObject(ObjectOutputStream s) throws IOException  {
  s.defaultWriteObject();                  // write fields to stream
  Hashtable hash = new Hashtable(3);
  if (model instanceof Serializable)
  hash.put("mo", model); // NORES
  if (viewManager instanceof Serializable)
  hash.put("vm", viewManager); // NORES
  if (selection instanceof Serializable)
  hash.put("se", selection); // NORES
  s.writeObject(hash);                    // write hash table to stream
}

private transient VectorModel             model;
private transient WritableVectorModel     writeModel;
private transient VectorViewManger        viewManager;
private transient WritableVectorSelection selection;
...

The model, writeModel, viewManager, and selection properties are all declared as transient. Each of these are interfaces, so a custom serialization procedure is needed to write these properties to the stream. writeObject() first calls the defaultWriteObject() method of the ObjectOutputStream to write all the fields that aren't marked as transient to the stream. It continues by writing the value of the model, viewManager, and selection properties to a hash table if each of these implement the Serializable interface. Finally, the hash table is written to the stream by calling the writeObject() method of ObjectOutputStream.

Defining readObject()

Within the readObject() method, write the custom code that reads your object from the stream. At any time, you can use Java's default deserialization by calling the defaultReadObject() method of the ObjectInputStream that is the argument of this readObject() method. The code that follows is the readObject() method of ListCore; you can see that it is reading the object from the stream with a call to defaultReadObject(), then handling the data extracted from the hash table that was stored on the stream with the writeObject() method:
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
  s.defaultReadObject();                        // read object from stream with default deserialization
  Hashtable hash = (Hashtable)s.readObject();   // call readObject() of s and downcast the data read ...
                                                // ... before assigning it to a variable 
  Object data = null;

  data = hash.get("mo"); // NORES               // get model property value
  if (data != null)
    model = (VectorModel)data;
  if (model instanceof WritableVectorModel)model;

  data = hash.get("vm"); // NORES               // get viewManager property value
  if (data != null)
    viewManager = (VectorViewManager)data;

  data = hash.get("se"); // NORES               // get selection property value
  if (data != null)
    selection = (WritableVectorSelection)data;
}

Serializing with the Externalizable interface

If you prefer to control the serialization and deserialization process completely, you can implement the Externalizable interface. Externalizable extends Serializable. It has two methods to implement: writeExternal() and readExternal(). Within these methods, you write the code that handles all the serialization and deserialization details.

To serialize an object using the Externalizable interface, pass the object to the writeObject() method of an ObjectOutputStream. writeObject() then executes the writeExternal() method. Similarly, to read an object from a stream, call the readObject() method of an ObjectInputStream. readObject() then executes your readExternal() method.

Using the Externalizable interface, you never use the Java serialization mechanism, but always create your own. For more information about externalizing your component, consult the JDK documentation.


Versioning

Sometimes the class that serializes an object differs slightly from the class that deserializes it. For example, suppose you make a variable transient in the class that serializes the object when it wasn't before, but no similar change is made in the class that deserializes the object. If an object is then serialized, the deserializing class will expect a variable but not find it. The result is that the deserializing class throws an InvalidClassException.

In serialization and deserization code that you write, you can often handle such discrepancies if you can convince the serialization mechanism that the two classes are merely two versions of the same class. To do that, you must define a serialVersionUID constant in the class.. serialVersionUID must be defined as static final long. For example, this is the current definition of serialVersionUID in ListCore:

private static final long serialVersionUID = 200L;
If you don't add a serialVersionUID variable, a unique version number for your class is created automatically by the ObjectOutputStream. Any change made to the class results in the generation of a new version number. To prevent this from happening, declare the serialVersionUID constant. Even if the serializing class and the deserializing class vary slightly, the version number is the same and therefore, the classes are considered to be the same version.

The JDK includes a serialver program that can compute an initial value for your class. By adding a serialVersionUID constant, you can handle minor changes to your class without destroying the compatibility of the two classes. When you make greater changes to the class, change serialVersionUID constant.