Most meaningful Java applications provide a way to save the Java objects they create and to restore these objects at a later point in time. For example, in Chapter 26, "Developing Beans," you saw how the BeanBox lets you change the properties of a component and then save the component's state. The BeanBox also lets you restore the state of a saved component.
The capability for an object to exist beyond the execution of the program that created it is known as persistence. Serialization is the key to implementing persistence, providing the capability to write an object to a stream and to read the object back in at a later time.
Serialization allows you to store objects in files, communicate them across networks, and use them in distributed applications. In this chapter, you'll be introduced to object serialization. You'll learn how it works and how it is used in both distributed and non-distributed applications. You'll cover the Serializable and Externalizable interfaces and the security issues related to object serialization. When you finish this chapter, you'll be better able to use serialization in your programs.
On the surface, the process of storing an object to a stream may seem trivial. But in fact, it is quite involved. When an object is written to a stream, information about its class must be stored along with the object. Without class information, there is no way to reconstruct an object that is read from a stream. In addition to class information, all objects that are referenced by that object must also be stored. If the referenced objects are not stored along with the object, the references of the stored object become mean-ingless.
The JDK 1.2 provides the Serializable and Externalizable interfaces of the java.io package to support object serialization. The Serializable interface is easy to use. It contains no methods. You just declare your class as being Serializable and use the ObjectOutputStream filter to write objects to a stream. The writeObject() method of ObjectOutputStream automatically takes care of storing the correct information when objects of the class are written to a stream. Objects are read back in using the readObject() method of the ObjectInputStream class. The objects that are read back in are cast to their original types.
Listing 40.1 provides an example of using the Serializable interface. The SerialApp program creates an instance of ObjectOutputStream on the temp file and assigns it to the outputStream variable. It invokes the getProperties() method of the System class to obtain the current system properties. It invokes the writeObject() method of ObjectOutputStream to write the Properties object to the output stream. Note that the Properties class is Serializable. It then closes the output stream and creates an instance of ObjectInputStream on the temp file. It reads an object from the input stream using the readObject() method of the ObjectInputStream class. The object is cast into an object of the Properties class and assigned to the newProp variable. The list() method of the Properties class displays the object as follows:
-- listing properties -- java.specification.name=Java Platform API Specification awt.toolkit=sun.awt.windows.WToolkit java.version=1.2beta2 java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment java.tmpdir=C:\WINDOWS\TEMP\ user.timezone=PST java.specification.version=1.2beta2 user.home=D:\JDK1.2BETA2\BIN\.. java-vm.name=non-JIT os.arch=x86 java.awt.fonts=C:\WINDOWS\Fonts java.vendor.url=http://www.sun.com/ user.region=US file.encoding.pkg=sun.io java.home=D:\JDK1.2BETA2\BIN\.. java-vm.specification.vendor=Sun Microsystems Inc. java-vm.specification.version=1.0 java.class.path=.;D:\JDK1.2BETA2\BIN\..\classes;D:\JD... line.separator= os.name=Windows 95 java.vendor=Sun Microsystems Inc. java.library.path=D:\JDK1.2BETA2\BIN;.;C:\WINDOWS;C:\WI... java-vm.version=1.2beta2 file.encoding=8859_1 java.specification.vendor=Sun Microsystems Inc. user.name=jaworskij user.language=en java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport... java.class.version=45.3 os.version=4.0 path.separator=; java-vm.specification.name=Java Virtual Machine Specification user.dir=D:\jdk1.2beta2\ju\ch36 file.separator=\ java-vm.vendor=Sun Microsystems Inc.
The properties that are displayed on your computer will differ depending on your operating system and how you have set up the JDK.
Java's object serialization capabilities make it easy to read and write objects from a file. Without these capabilities, you would be forced to read and write each primitive value of each property.
import java.io.*; import java.util.*; public class SerialApp { public static void main(String args[]) { try { ObjectOutputStream outputStream = new ObjectOutputStream( new FileOutputStream("temp")); Properties prop = System.getProperties(); outputStream.writeObject(prop); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream( new FileInputStream("temp")); Properties newProp = (Properties) inputStream.readObject(); inputStream.close(); newProp.list(System.out); } catch(Exception ex) { System.out.println(ex.toString()); } } }
Special Requirements for Using Serializable
Only objects that are Serializable can be stored to a stream using the ObjectOuputStream class. This includes all objects that are referenced by the object being written. If the object being written to a stream refers to an object that is not Serializable, the NotSerializableException is thrown.
When a serialized object is written to a stream, the class of the object, the class's signature, and the values of all non-transient and non-static field variables are written to the stream. If an object references other objects (except in transient or static fields), those objects are also written to the stream.
NOTE: TStatic and transient field variables are not saved when a Serializable object is written to a stream using the writeObject(). These field variables are ignored by readObject() when a Serializable object is read from a stream.
In order for a class to be Serializable, its parent class must either be Serializable or have a default constructor that does not take any arguments. The no-argument constructor is used to initialize an object's parent class when the object is read from a stream. If a parent class is not Serializable, it is the responsibility of the Serializable subclass to save and restore the state of its parent beyond the default initialization. A Serializable class can tailor the way that it is serialized by declaring readObject() and writeObject()methods of the following form:
private void writeObject(ObjectOutputStream stream) throws IOException { } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { }
In general, because all primitive types and most classes are Serializable, you don't need to mess around with tailoring the way that your classes are serialized.
You can determine if a class is Serializable by looking up its API description or writing a program to see if its objects are instances of the Serializable interface. In some cases, the first alternative may not be possible--you may be using a class that is not documented. The second alternative is somewhat of an inconvenience. The serialver tool that is included with the JDK 1.2 provides a quick and easy way to determine if a class is Serializable. It also returns the serialVersionUID of the class. The serialVersionUID is used to uniquely identify the class of an object that is written to a stream. The serialver tool can be run from the command line by identifying the fully qualified name of a class as an argument. Examples of its use follow:
serialver java.lang.Object Class java.lang.Object is not Serializable. serialver java.lang.String java.lang.String: static final long serialVersionUID = Â-6849794470754667710L;
You can also run serialver as a window application by invoking it with the -show option. For example, serialver -show creates the application window shown in Figure 40.1. You can then enter the name of the class you want to check and click the Show button. The program displays the results of the query, as shown in Figures 40.2 and 40.3.
FIGURE 40.1. The window version of serialver.
FIGURE 40.2. The serialver program identifies the serialVersionUID of a class.
FIGURE 40.3. The serialver program identifies a class as not being Serializable.
The Serializable interface and the writeObject() and readObject() methods of ObjectOutputStream and ObjectInputStream provide the most convenient way to save objects to a stream and restore them at a later time. This convenience comes at the expense of relinquishing control of how the objects are stored in and read from streams.
The Externalizable interface extends the Serializable interface to provide you with additional control over how objects are written to and read from streams. Because the Externalizable interface extends the Serializable interface, you can use Externalizable objects with ObjectOutputStream and ObjectInputStream in the same manner that you could use Serializable objects. The only difference being that the class of an Externalizable object must implement the writeExternal() and readExternal() methods defined by the Externalizable interfaces. These methods are of the following form:
public void writeExternal(ObjectOutput out) throws IOException { } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }
The writeExternal() method is implemented by an Externalizable object to write itself to an output stream represented by an ObjectOutput interface. The ObjectOutput interface provides methods for writing objects to an output stream. The ObjectOutput interface extends the DataOutput interface, which provides methods for writing primitive types to an output stream.
The readExternal() method is the input analog of the writeExternal() method. It is implemented by an Externalizable object to read its values from an input stream represented by an ObjectInput interface. The ObjectInput interface provides methods for reading objects from an input stream. It extends the DataInput interface, which provides methods for reading primitive data types from an input stream. When an Externalizable object is read from an input stream, an instance of the object is created using its default no-argument constructor. The object's readExternal() method is then invoked.
The Externalizable interface gives you complete control over the way that objects are written to and read from streams. This control comes at the expense of having to implement your own methods for reading and writing the objects. The Exteralizable objects are responsible for storing and retrieving the state of their parent class, as well as any objects that they reference.
Listing 40.2 provides an example of using the Externalizable interface. The main() method of ExternalApp is similar to the main() method of SerialApp. The only difference is that you are writing and reading an object of MyClass.
The MyClass class implements the Externalizable interface. It declares three variables that are of the GregorianCalendar and String types. Its no-argument constructor creates a default GregorianCalendar object and two strings that are set to uninitialized. Its other constructor takes a Date parameter. It invokes the setFieldValues() method to set the cal, timeVal, and dateVal variables based on the value of the Date object that is passed as a parameter.
The writeExternal() method saves a MyClass object to a stream as a Date object. The Date object is created using the getTime() method of the Calendar class. The readExternal() method reads a MyClass object as a Date object and invokes the setFieldValues() method to initialize the fields of the MyClass object.
The toString() method returns a String value of a MyClass object. The setFieldValues() method initializes the field variables of a MyClass object based on the value of a Date parameter.
The ExternalApp program shows how the Externalizable interface is used to control the manner in which objects are serialized. The writeExternal() and readExternal() methods serialize MyClass objects as Date objects. This allows these objects to be stored in a more compact form.
The program displays the current date and time as shown:
java ExternalApp 1/15/1998 2:5:4
import java.util.*; public class ExternalApp { public static void main(String args[]) { try { ObjectOutputStream outputStream = new ObjectOutputStream( new FileOutputStream("temp")); MyClass obj = new MyClass(new Date()); outputStream.writeObject(obj); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream( new FileInputStream("temp")); MyClass newObj = (MyClass) inputStream.readObject(); inputStream.close(); System.out.println(newObj); } catch(Exception ex) { System.out.println(ex.toString()); } } } class MyClass implements Externalizable { GregorianCalendar cal; String timeVal; String dateVal; public MyClass() { cal = new GregorianCalendar(); timeVal="uninitialized"; dateVal="uninitialized"; } public MyClass(Date d){ cal = new GregorianCalendar(); setFieldValues(d); } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(cal.getTime()); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { Date date = (Date) in.readObject(); setFieldValues(date); } public String toString() { if(timeVal=="uninitialized") return timeVal; return dateVal+" "+timeVal; } void setFieldValues(Date d){ cal.setTime(d); timeVal = String.valueOf(cal.get(cal.HOUR))+":"; timeVal += String.valueOf(cal.get(cal.MINUTE))+":"; timeVal += String.valueOf(cal.get(cal.SECOND)); dateVal = String.valueOf(cal.get(cal.MONTH)+1)+"/"; dateVal += String.valueOf(cal.get(cal.DATE))+"/"; dateVal += String.valueOf(cal.get(cal.YEAR)); } }
Object serialization is used extensively in remote method invocation. It is used to send the arguments of a method invocation from the client object to the remote object, and to send return values from the server object back to the client object. As a consequence, all arguments to a remote method invocation must be Serializable. Similarly, any values returned by a remote object must also be Serializable.
The serialization used by RMI is transparent to the client and server objects. It is performed by the stubs and skeletons, remote reference layer, and transport layer. This is a great convenience and a major benefit of RMI. Applications that use TCP sockets for client/server communication are responsible for serializing and deserializing objects via input and output streams. With RMI, you only need to ensure that your arguments and return values are Serializable, and RMI takes care of serialization and deserialization. Refer to Chapter 39, "Remote Method Invocation."
Because object serialization is used as the primary mechanism to store and retrieve Java objects and to send them over a network, it is of considerable importance to the security of Java objects and programs. While a Java object is stored in a file or is being transmitted over a network, it is outside of the Java Virtual Machine and is vulnerable to deliberate or accidental modification. In addition, any sensitive data contained in the object can be read.
Whenever an object is read from a stream, the object cannot be trusted to be a faithful representation of the object that was written to the stream. The validity of an object must be determined when it is read. This can be accomplished by implementing the readObject() and writeObject() or readExternal() and writeExternal() methods. A digital signature of the object can be created, using the methods of the Security API, and stored with the object. The object's signature can be verified when the object is read from the stream.
When sensitive information contained in an object is written to a stream, that information is subject to disclosure. One solution to this problem is to encrypt objects when they are serialized. This can be readily accomplished using the Security API by creating output and input streams that act as filters for the ObjectOutputStream and ObjectInputStream classes. Refer to Chapter 3, "The Extended Java Security Model," and Chapter 8, "Applet Security," for information on using encryption with Java.
Another solution involves the judicious use of static and transient variables. Because static and transient variables are never serialized, the information contained in these variables is never exposed. The drawback to this approach is that the sensitive information is not persistent and expires at the termination of the program that created it. An even harsher solution would be to ensure that sensitive objects do not implement the Serializable or Externalizable interfaces.
JavaSoft is currently researching new approaches to building distributed systems. Part of this research is referred to as JavaSpaces and is aimed at providing more advanced mechanisms to supported distributed object persistence and data exchange. The JavaSpaces model is shown in Figure 40.4. In this model, JavaSpaces are objects that hold other objects called entries. Each entry is a typed group of objects that is stored in the JavaSpace. Entries can be written to, read from, and deleted from a JavaSpace. JavaSpaces can be searched to find entries using templates. Templates are entries whose fields are set to search patterns. A JavaSpace can also provide notification of events related to entries. JavaSpaces support secure transaction processing and eliminate the need for distributed coordination between objects that concurrently access a JavaSpace. Access controls are performed by identifying the objects that make requests of a particular JavaSpace and entry.
FIGURE 40.4. The JavaSpaces model.
JavaSpaces are useful for building distributed applications because they allow distributed objects to be stored in a persistent manner and managed independently of the applications they support. They provide distributed access controls, enable distributed event processing, and support flow of objects application models. Flow of objects models implement work flows using objects that are read to and taken from JavaSpaces. JavaSpaces is not part of JDK 1.2 but may be integrated within future JDK releases.
In this chapter, you were introduced to object serialization. You learned how it works and how it is used in both distributed and non-distributed applications. You learned about the Serializable and Externalizable interfaces and about security issues related to object serialization. You also learned about the capabilities that will be provided by JavaSpaces. In the next chapter, you'll extend your distributed application programming capabilities by learning how Java fits into the Common Object Request Broker Architecture (CORBA).
© Copyright, Macmillan Computer Publishing. All rights reserved.