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.
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.
/* * * 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.
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?
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.
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.
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
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.
Listing 39.4 shows an example program called DateWrite. DateWrite creates a Date Object and writes the entire 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.javaNOTE: 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.
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.
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.
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.
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.
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
Object serialization is not limited to applications. Listing 39.6 shows DateRead changed so that it can also be run as an applet.
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.
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.
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; } }
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"); } } }
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.