Chapter 39
Object Serialization

by Joe Weber

Up to this point you have been working with Objects, and you have learned to create classes so you can manipulate the Objects using their methods. However, when you have come to write an Object to a different source, say out to a Network via a Socket or to a file, you have only written out native types like int or char. Object Serialization is the tool that was added to the JDK 1.1 to allow you to fully utilize the OOP nature of Java and write those Objects you've labored to produce to a file.

Object Serialization

To understand Object Serialization, first look at an example of how you would go about reading in a simple object, such as a string, from another source. Normally when you open a stream to and from a client program, the odds are fairly good that you are sending/receiving a byte. You're probably then adding that byte to a string. To do this you might have some code similar to that in Listing 39.1.

Listing 39.1Notice How Much Work the Computer Has to Do to Generate a String This Way


/*
 *
 * GetString
 *
 */

import java.net.*;
import java.io.*;

public class GetString 
{
     
     //Read in a String from an URL
     public String getStringFromUrl (URL inURL){
          InputStream in;
          try{
               in = inURL.openStream();
          } catch (IOException ioe){
               System.out.println("Unable to open stream to URL:"+ioe);
               return null;
          }
          return getString(in);
     }

     public String getStringFromSocket (Socket inSocket){
          InputStream in;
          try{
               in = inSocket.getInputStream();
          } catch (IOException ioe){
               System.out.println("Unable to open stream to Socket:"+ioe);
               return null;
          }
          return getString(in);
     }

     public String getString (InputStream inStream){
          String readString = new String();
          DataInputStream in = new DataInputStream (inStream);
          char inChar;
          try{
               while (true){
                    inChar = (char)in.readByte();
                    readString = readString + inChar;
               }
          } catch (EOFException eof){
               System.out.println("The String read was:"+readString);
          } catch (IOException ioe) {
               System.out.println("Error reading from stream:"+ioe);
          }
          return readString;
     }
}

Most important in Listing 39.1, take a look at the getString() method. Inside of this method you will see an indefinitely long while loop (which breaks once an exception is thrown). If you look closely at what is happening here you will realize you are reading character-by-character each letter in the string and appending it until you reach the end of the file (EOF). Java has no way without Object Serialization to actually read in a string as an object.


NOTE: DataInputStream does have a readLine() which returns a String, but this is not really the same for two reasons. First, readLine does not read in an entire file; second, the readLine() method itself is actually very similar to readString() in Listing 39.1


An even more dire situation arises when you want to read a heterogeneous object such as that shown in Listing 39.2.

Listing 39.2A Heterogeneous Object

class testObject {
     int x;
     int y;
     float angle;
     String name;
     public testObject (int x, int y, float angle, String name){
     this.x = x ;
     this.y = y;
     this.angle= angle;
          this.name = name;
}
} 

To read and write testObject without Object Serialization you would likely open a stream, read in a bunch of data, and then use it to fill out the contents of a new object (by passing the read-in elements to the constructor). You might even be able to deduce directly how to read in the first three elements of testObject. But how would you read in the name? Well, since you just wrote a readString class in Listing 39.1 you could use that, but how would you know when the string ends and the next object starts? Even more importantly, what if testObject had even more complicated references? For instance, if testObject looked like Listing 39.3, how would you handle the constant recursion from nextObject?

Listing 39.3testObject Becomes Even More Complicated

class testObject {
     int x;
     int y;
     float angle;
     String name;
     testObject nextNode;
     public testObject (int x, int y, float angle, String name, testObject nextNode){
     this.x = x ;
             this.y = y;
             this.angle= angle;
          this.name = name;
                         this.nextNode = nextNode;
        }
}

If you really wanted to, you could write a method (or methods) to read and write Listing 39.3, but wouldn't it be great if, instead, you could grab an object a whole class at a time?

That's exactly what object serialization is all about. Do you have a class structure that holds all of the information about a house for a real estate program? No problem--simply open the stream and send or receive the whole house. Do you want to save the state of a game applet? Again, no problem. Just send the applet object down the stream.

The ability to store and retrieve whole objects is essential to the construction of all but the most ephemeral of programs. While a full-blown database might be what you need if you're storing large amounts of data, frequently that's overkill. Even if you want to implement a database, it would be easier to store objects as BLOB types (byte streams of data) than to break out an int here, a char there, and a byte there.

How Object Serialization Works

The key to object serialization is to store enough data about the object to be able to reconstruct it fully. Furthermore, to protect the user (and programmer), the object must have a "fingerprint" that correctly associates it with the legitimate object from which it was made. This is an aspect not even discussed when looking at writing our own objects. But it is critical for a complete system so that when an object is read in from a stream, each of its fields will be placed back into the correct class in the correct location.


NOTE: If you are a C or C++ programmer, you're probably used to accomplishing much of object serialization by taking the pointer to a class or struct, doing a sizeOf(), and writing out the entire class. Unfortunately, Java does not support pointers or direct-memory access, so this technique will not work in Java, and object serialization is required.


It's not necessary, however, for a serialization system to store the methods or the transient fields of a class. The class code is assumed to be available any time these elements are required. In other words, when you restore the class Date, you are not also restoring the method getHours(). It's assumed that you have restored the values of the Date into a Date object and that object has the code required for the getHours() method.

Dealing with Objects with Object References

Objects frequently refer to other objects by using them as class variables (fields). In other words, in the more complicated testObject class (refer to Listing 39.3), a nextNode field was added. This field is an Object referenced within the object. In order to save a class, it is also necessary to save the contents of these reference objects. Of course, the reference objects may also refer to yet even more objects (such as with testObject if the nextNode also had a valid nextNode value). So, as a rule, to serialize an object completely, you must store all of the information for that object, as well as every object that is reachable by the object, including all of the recursive objects.


CAUTION:
Object serialization and Remote Method Invocation are not available under Netscape Navigator 3.0, Microsoft Internet Explorer 3.0, or the JDK 1.0. The first version of the JDK that supports object serialization and RMI is JDK 1.02, and that only with a patch. It is recommended that you use the JDK 1.1 when trying any of these features. If you are using the JDK 1.02, you must obtain the Object Serialization and RMI classes separately, and these must be added to your existing class library. These classes can be downloaded from the following URL:

http://chatsubo.javasoft.com/current/download.html


Object Serialization Example

As a simple example, store and retrieve a Date class to and from a file. To do this without object serialization, you would probably do something on the order of getTime() and write the resulting long integer to the file. However, with object serialization the process is much, much easier.

An Application to Write a Date Class

Listing 39.4 shows an example program called DateWrite. DateWrite creates a Date Object and writes the entire Object to a file.

Listing 39.4DateWrite.java--An Application that Writes a Date Object to a File

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
public class DateWrite {
        public static void main (String args[]){
               try{
                    // Serialize today's date to a file.
                    FileOutputStream outputFile = new 						FileOutputStream("dateFile");
                    ObjectOutputStream  serializeStream  =  new
ObjectOutputStream(outputFile);
                  serializeStream.writeObject("Hi!");
                  serializeStream.writeObject(new Date());
                  serializeStream.flush();
            } catch (Exception e) {
                  System.out.println("Error during serialization");
            }
      }
}//end class DateWrite

Take a look at the code in Listing 39.4. First, notice that the program creates a FileOutputStream. In order to do any serialization it is first necessary to declare an outputStream of some sort to which you will attach the ObjectOutputStream. (As you see later in Listing 39.5, you can also use the OutputStream generated from any other object, including an URL.)

Once you have established a stream, it is necessary to create an ObjectOutputStream with it. The ObjectOutputStream contains all of the necessary information to serialize any object and to write it to the stream.

In the example of the previous short code fragment, you see two objects being written to the stream. The first object that is written is a String object; the second is the Date object.


NOTE: To compile Listing 39.4 using the JDK 1.02, you need to add some extra commands that you're probably not used to. Before you do this, though, first verify that you have downloaded the RMI/object serialization classes and unzipped the file into your Java directory. Now, type the command:

javac -classpathc:\java\lib\classes.zip;c:\java\lib\objio.zip;. DateWrite.java

NOTE: The previous compiler command assumes you are using a Windows machine and that the directory in which your Java files exist is C:\JAVA. If you have placed it in a different location or are using a system other than Windows, you need to substitute C:\JAVA\LIB with the path that is appropriate for your Java installation. As always, it's a good idea to take a look at the README file included with your installation, and to read the release notes to learn about any known bugs or problems.

NOTE: This should compile DateWrite cleanly. If you receive an error, though, make sure that you have a OBJIO.ZIP file in your JAVA\LIB directory. Also, make sure that you have included both the CLASSES.ZIP and the OBJIO.ZIP files in your class path.


Running DateWrite Under JDK 1.02

Once you have compiled the DateWrite program you can run it. However, just as you had to include the OBJIO.ZIP file in the classpath when you compiled the DateWrite class, you must also include it in order to run the class.

java -classpath c:\java\lib\classes.zip;c:\java\lib\objio.zip;. DateWrite


NOTE: If you fail to include the OBJIO.ZIP file in your class path, you will likely get an error such as:

java.lang.NoClassDefFoundError: java/io/ObjectOutputStream
at DateWrite.main (DateWrite.java: 9)




This is the result of the virtual machine being unable to locate the class files that are required for object serialization.


Compiling and Running DateWrite

To compile and run DateWrite using the JDK 1.1 simply copy the contents of Listing 39.4 to a file called DateWrite and compile it with javac as you would any other file:

javac DateWrite.java

You can run it just as you would any other Java Application as well:

java DateWrite

No real output is generated by the DateWrite class, so when you run this program you should be returned to the command prompt fairly quickly. However, if you now look in your directory structure, you should see a file called dateFile. This is the file you just created. If you attempt to type out the file, you will see something that looks mostly like gobbledy-gook.

However, a closer inspection reveals that this file contains several things. The stuff that looks like gobbledy-gook is actually what the serialization uses to store information about the class, such as the value of the fields and the class signature that was discussed earlier.

A Simple Application to Read in the Date

The next step, of course, is to read the Date and String back in from the file. See how complicated this could be. Listing 39.5 shows an example program that reads in the String and Date.

Listing 39.5DateRead.java--An Application that Reads the String and Date Back in


import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Date;
public class DateRead {
      public static void main (String args[]){
            Date wasThen;
            String theString;
            try{
                  // Serialize today's date to a file.
                  FileInputStream inputFile = new FileInputStream("dateFile");
                  ObjectInputStream  serializeStream  =  new
            ObjectInputStream(inputFile);
                  theString  = (String) serializeStream.readObject();
            } catch (Exception e) {
                  System.out.println("Error during serialization");
                  return; 
            }
            System.out.println("The string is:"+theString);
            System.out.println("The old date was:"+wasThen);
        }
}

Listings 39.4 and 39.5 differ primarily in the ways that you would expect. Listing 39.4 is writing, and Listing 39.5 is reading. In DateRead, you first declare two variables to store the objects in. You need to remember to do this, because if you were to create the variables inside the try-catch block, they would go out of scope before reaching the System.out line. Next, a FileInputStream and ObjectInputStream are created, just as the FileOutputStream and ObjectOutputStreams were created for DateWrite.

The next two lines of the code are also probably fairly obvious, but pay special attention to the casting operator. readObject() returns an Object class. By default, Java does not polymorph-cast any object, so you must implicitly direct it to do so. The rest of the code should be fairly obvious to you by now.

You can compile and run DateRead, simply follow the same directions for DateWrite.


NOTE: To compile the code using JDK 1.02, this time set a classpath variable so that you don't always have to use the -classpath option with javac. You can use the -classpath option as done in the previous example, but this solution is a bit more efficient. In either case these solutions are interchangeable. To set the classpath this way do this:

On a Windows machine, type:

set classpath=c:\java\lib\classes.zip;c:\java\lib\objio.zip;.


On other platforms, the syntax is slightly different. For instance, under UNIX you might type:

classpath=/usr/java/lib/classes.zip:/usr/java/lib/objio.zip:.


export classpath

In either case, don't forget to add the current directory (.) to the end of the classpath statement. javac will run without the current directory being listed, but the java command won't work.


Compiling and Running DateRead

You can compile and run DateRead, simply follow the same directions for DateWrite.

javac DateRead.java

You can also run it just by using the familiar java command:

java DateRead

Here's an example of the resulting output from this code:

The String is:Hi!
The old date was:Wed Dec 1 23:36:26 edt 1996

Notice that the String and Date are read in just as they were when you wrote them out. Now you can write out and read entire objects from the stream without needing to push each element into the stream.


CAUTION:
As you may have already guessed, it is imperative that you read in objects in exactly the same order as you wrote them out. If you fail to do this, a runtime error will occur that will say something such as the following:

Error during serialization


Reading In the Date with an Applet

Object serialization is not limited to applications. Listing 39.6 shows DateRead changed so that it can also be run as an applet.

Listing 39.6DataReadApp.java--An Applet that Reads a Date Object to a File

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Date;
import java.awt.Graphics;
public class DateReadApp extends java.applet.Applet {
      public void paint (Graphics g){
            Date wasThen;
            String theString;
            try{
                  // Serialize today's date to a file.
                  FileInputStream inputFile = new FileInputStream("dateFile");
                  ObjectInputStream  serializeStream  =  new
            ObjectInputStream(inputFile);
                  theString  = (String) serializeStream.readObject();
                  wasThen = (Date)serializeStream.readObject();
            } catch (Exception e) {
                  System.out.println("Error during serialization");
                  return;
            }
            g.drawString(("The string is:"+theString),5,100);
            g.drawString(("The old date was:"+wasThen),5,150);
        }
}

Once you have compiled Listing 39.6, the resulting output should look like Figure 39.1. Remember that you will have to use AppletViewer to run this applet, because other browsers don't yet support object serialization.

FIG. 39.1
The Date and String have been read in using serialization.


NOTE: While you can run DateReadApp with AppletViewer, you cannot run it using Netscape because some changes needed to be made to the virtual machine in order to make object serialization possible. These changes have not yet been adopted by Netscape.


Writing and Reading Your Own Objects

By default, you have the ability to write and read most of your own objects, just as you did with the Date class. There are certain restrictions right now (such as if the Object refers to a native peer), but for the most part, any class that you create can be serialized.

Listings 39.7 through 39.9 show the source code for serializing an example class called SerializeObject.

Listing 39.7SerializeObject--A Simple Class with a Couple of Fields

public class SerializeObject implements java.io.Serializable{
      public int first;
      public char second;
      public String third;
      public SerializeObject (int first, char second, String third){
            this.first= first;
            this.second = second;
            this.third = third;
      }
}

Listing 39.8ObjWrite--Write out a Sample SerializeObject to a File

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import SerializeObject;
public class ObjWrite {
      public static void main (String args[]){
            try{
                  // Serialize today's date to a file.
                  FileOutputStream outputFile = new FileOutputStream("objFile");
                  ObjectOutputStream  serializeStream  =  new
            ObjectOutputStream(outputFile);
                  SerializeObject obj = new SerializeObject (1,'c',new String ("Hi!"));
                  serializeStream.writeObject(obj);
                  serializeStream.flush();
            } catch (Exception e) {
                  System.out.println("Error during serialization");
            }
      }
}

Listing 39.9ObjRead--Read in the Same Object from the File

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import SerializeObject;
public class ObjRead extends java.applet.Applet {
      public void init(){
            main(null); 
      }

      public static void main (String args[]){
            SerializeObject obj;
            try{
                  // Serialize today's date to a file.
                  FileInputStream inputFile = new FileInputStream("objFile");
                  ObjectInputStream  serializeStream  =  new
            ObjectInputStream(inputFile);
                  obj = (SerializeObject)serializeStream.readObject();
            } catch (Exception e) {
                  System.out.println("Error during serialization");
                  return;
            }
            System.out.println("first is:"+obj.first);
            System.out.println("second is:"+obj.second);
            System.out.println("third is:"+obj.third);
      }
}

In the previous example classes, notice that the SerializeObject class refers to a number of things, including another class--String. As you might already suspect, once you have compiled and run each of these classes, the resulting output is:

First is:1
Second is:c
Third is:Hi!

What's most amazing about all of this code is how easy it is to transfer the object.