Object Serialization and Deserialization

Although you can use the data stream classes to stream primitive type values and String objects, you cannot use these classes to stream non-String objects. Instead, you must use object serialization and deserialization to stream objects of arbitrary types.

Object serialization is a virtual machine mechanism for serializing object state into a stream of bytes. Its deserialization counterpart is a virtual machine mechanism for deserializing this state from a byte stream.

NOTE: An object's state consists of instance fields that store primitive type values and/or references to other objects. When an object is serialized, the objects that are part of this state are also serialized (unless you prevent them from being serialized). Furthermore, the objects that are part of those objects' states are serialized (unless you prevent this), and so on.

Java supports three forms of serialization and deserialization: default serialization and deserialization, custom serialization and deserialization, and externalization.

Default Serialization and Deserialization

Default serialization and deserialization is the easiest form to use but offers little control over how objects are serialized and deserialized. Although Java handles most of the work on your behalf, there are a couple of tasks that you must perform.

Your first task is to have the class of the object that is to be serialized implement the Serializable interface (directly, or indirectly via the class's superclass). The rationale for implementing Serializable is to avoid unlimited serialization.

NOTE: Serializable is an empty marker interface that a class implements to tell the virtual machine that it is okay to serialize the class's objects. When the serialization mechanism encounters an object whose class does not implement Serializable, it throws an instance of the NotSerializableException class (an indirect subclass of IOException).

Unlimited serialization is the process of serializing an entire object graph (all objects that are reachable from a starting object). Java does not support unlimited serialization for the following reasons:

■ Security: If Java automatically serialized an object containing sensitive information (such as a password or a credit card number), it would be easy for a hacker to discover this information and wreak havoc. It is better to give the developer a choice to prevent this from happening.

■ Performance: Serialization leverages the Reflection API, introduced in Chapter 7. In that chapter, you learned that reflection slows down application performance. Unlimited serialization could really hurt an application's performance.

■ Objects not amenable to serialization: Some objects exist only in the context of a running application and it is meaningless to serialize them. For example, a file stream object that is deserialized no longer represents a connection to a file.

Listing 10-19 declares an Employee class that implements the Serializable interface to tell the virtual machine that it is okay to serialize Employee objects.

Listing 10-19. Implementing Serializable public class Employee implements java.io.Serializable

private String name; private int age;

public Employee(String name, int age) {

public String getName() { return name; } public int getAge() { return age; }

Because Employee implements Serializable, the serialization mechanism will not throw a NotSerializableException instance when serializing an Employee object. Not only does Employee implement Serializable, the String class also implements this interface.

Your second task is to work with the ObjectOutputStream class and its writeObject() method to serialize an object, and the OutputlnputStream class and its readObject() method to deserialize the object.

NOTE: Although ObjectOutputStream extends OutputStream instead of FilterOutputStream, and although ObjectlnputStream extends InputStream instead of FilterlnputStream, these classes behave as filter streams.

Java provides the concrete ObjectOutputStream class to initiate the serialization of an object's state to an object output stream. This class declares an ObjectOutputStream(OutputStream out) constructor that chains the object output stream to the output stream specified by out.

When you pass an output stream reference to out, this constructor attempts to write a serialization header to that output stream. It throws NullPointerException when out is null, and IOException when an I/O error prevents it from writing this header.

ObjectOutputStream serializes an object via its void writeObject(Object obj) method. This method attempts to write information about obj's class followed by the values of obj's instance fields to the underlying output stream.

writeObject() does not serialize the contents of static fields. In contrast, it serializes the contents of all instance fields that are not explicitly prefixed with the transient reserved word. For example, consider the following field declaration:

public transient char[] password;

This declaration specifies transient to avoid serializing a password for some hacker to encounter. The virtual machine's serialization mechanism ignores any instance field that is marked transient.

writeObject() throws IOException or an instance of an IOException subclass when something goes wrong. For example, this method throws NotSerializableException when it encounters an object whose class does not implement Serializable.

NOTE: Because ObjectOutputStream implements DataOutput, it also declares methods for writing primitive type values and strings to an object output stream.

Java provides the concrete ObjectInputStream class to initiate the deserialization of an object's state from an object input stream. This class declares an ObjectInputStream(InputStream in) constructor that chains the object input stream to the input stream specified by in.

When you pass an input stream reference to in, this constructor attempts to read a serialization header from that input stream. It throws NullPointerException when in is null, IOException when an I/O error prevents it from reading this header, and StreamCorruptedException (an indirect subclass of IOException) when the stream header is incorrect.

ObjectInputStream deserializes an object via its Object readObject() method. This method attempts to read information about obj's class followed by the values of obj's instance fields from the underlying input stream.

readObject() throws java.lang.ClassNotFoundException, IOException, or an instance of an IOException subclass when something goes wrong. For example, this method throws OptionalDataException when it encounters primitive values instead of objects.

NOTE: Because ObjectInputStream implements DataInput, it also declares methods for reading primitive type values and strings from an object input stream.

Listing 10-20 presents an application that uses these classes to serialize and deserialize an instance of Listing 10-19's Employee class to and from an employee.dat file.

Listing 10-20. Serializing and deserializing an Employee object import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream;

public class SerializationDemo {

final static String FILENAME = "employee.dat";

public static void main(String[] args) {

ObjectOutputStream oos = null; ObjectInputStream ois = null;

FileOutputStream fos = new FileOutputStream(FILENAME);

oos = new ObjectOutputStream(fos);

Employee emp = new Employee("John Doe", 36);

oos.writeObject(emp);

FileInputStream fis = new FilelnputStream(FILENAME); ois = new ObjectlnputStream(fis);

emp = (Employee) ois.readObject(); // (Employee) cast is necessary. ois.close();

System.out.println(emp.getName()); System.out.println(emp.getAge());

catch (ClassNotFoundException cnfe) {

System.err.println(cnfe.getMessage()); closeFiles(oos, ois);

catch (IOException ioe) {

System.err.println(ioe.getMessage()); closeFiles(oos, ois);

static void closeFiles(ObjectOutputStream oos, ObjectInputStream ois) {

catch (IOException ioe) {

System.err.println(ioe.getMessage());

catch (IOException ioe) {

System.err.println(ioe.getMessage());

Most of the source code is taken up with exception handling and closing the underlying file streams. The crucial code (shown in bold) is much briefer and demonstrates the tasks of default serialization and deserialization.

When you run this application, you will discover a file named employee.dat and observe the following output:

John Doe 36

There is no guarantee that the same class will exist when a serialized object is deserialized (perhaps an instance field has been deleted). During deserialization, this mechanism causes readObject() to throw an instance of InvalidClassException (an indirect subclass of IOException) when it detects a difference between the deserialized object and its class.

Every serialized object has an identifier. The deserialization mechanism compares the identifier of the object being deserialized with the serialized identifier of its class (all serializable classes are automatically given unique identifiers unless they explicitly specify their own identifiers) and causes InvalidClassException to be thrown when it detects a mismatch.

Perhaps you have added an instance field to a class, and you want the deserialization mechanism to set the instance field to a default value rather than have readObject() throw an InvalidClassException instance. (The next time you serialize the object, the new field's value will be written out.)

You can avoid the thrown InvalidClassException instance by adding a static final long serialVersionUID = long integer value; declaration to the class. The long integer value must be unique and is known as a stream unique identifier (SUID).

During deserialization, the virtual machine will compare the deserialized object's SUID to its class's SUID. If they match, readObject() will not throw InvalidClassException when it encounters a compatible class change (such as adding an instance field). However, it will still throw this exception when it encounters an incompatible class change (such as changing an instance field's name or type).

NOTE: Whenever you change a class in some fashion, you must calculate a new SUID and assign it to serialVersionUID.

The JDK provides a serialver tool for calculating the SUID. For example, to generate an SUID for Listing 10-19's Employee class, change to the directory containing Employee.class and execute serialver Employee. In response, serialver generates the following output, which you paste (except for Employee:) into Employee.java:

Employee: static final long serialVersionUID = 1517331364702470316L;

The Windows version of serialver also provides a graphical user interface that you might find more convenient to use. To access this interface, specify the -show command-line option. For example, Figure 10-4 reveals this user interface in the context of the Employee class.

Serial Version Inspector

□00

Serial Version: static final long serialVersionUID

=1517331364702470316L:

Figure 10-4. The serialver graphical user interface

Figure 10-4. The serialver graphical user interface

Custom Serialization and Deserialization

My previous discussion focused on default serialization and deserialization (with the exception of marking an instance field transient to prevent it from being included during serialization). However, situations arise where you need to customize these tasks.

For example, suppose you want to serialize instances of a class that does not implement Serializable. As a workaround, you subclass this other class, have the subclass implement Serializable, and forward subclass constructor calls to the superclass.

Although this workaround lets you serialize subclass objects, you cannot deserialize these serialized objects when the superclass does not declare a noargument constructor, which is required by the deserialization mechanism.

Consider java.util.StringTokenizer. This concrete class does not implement Serializable and does not declare a noargument constructor. Listing 10-21 subclasses StringTokenizer and proves that serialized subclass instances cannot be deserialized.

Listing 10-21. Problematic deserialization import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable;

import java.util.StringTokenizer;

class SerializableStringTokenizer extends StringTokenizer implements Serializable

SerializableStringTokenizer(String str) {

super(str);

SerializableStringTokenizer(String str, String delim) {

super(str, delim);

SerializableStringTokenizer(String str, String delim, boolean returnDelims)

super(str, delim, returnDelims);

public class SerializationDemo {

public static void main(String[] args) {

SerializableStringTokenizer sst;

sst = new SerializableStringTokenizer("The quick brown fox"); System.out.println("Number of tokens = " + sst.countTokens()); System.out.println("First token = " + sst.nextToken()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(sst); // Line 40 oos.close();

System.out.println("sst object written to byte array"); ByteArrayInputStream bais;

bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object o = ois.readObject(); // Line 46 System.out.println("sst object read from byte array");

catch (Exception e) {

e.printStackTrace();

Listing 10-21's main() method instantiates SerializableStringTokenizer with a sample string argument. SerializableStringTokenizer(String) passes this argument to its StringTokenizer counterpart, which assumes that tokens are delimited with spaces.

main() next calls StringTokenizer's countTokens() method to return the number of tokens in the string, and its nextToken() method to return the first token. Both values are output to the standard output device.

Continuing, main() works with the ByteArrayOutputStream and ByteArrayInputStream classes to provide a byte array as a stream destination and source. An instance of the SerializableStringTokenizer class is serialized to and deserialized from this array.

When you run this application, it generates the following output:

Number of tokens = 4

First token = the sst object written to byte array java.io.InvalidClassException: SerializableStringTokenizer; ^ SerializableStringTokenizer; no valid constructor at java.io.ObjectStreamClass.checkDeserialize(Unknown Source) at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

at java.io.ObjectInputStream.readObject0(Unknown Source) at java.io.ObjectInputStream.readObject(Unknown Source) at SerializationDemo.main(SerializationDemo.java:46) Caused by: java.io.InvalidClassException: SerializableStringTokenizer; no valid^ constructor at java.io.ObjectStreamClass.<init>(Unknown Source) at java.io.ObjectStreamClass.lookup(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.writeObject(Unknown Source) at SerializationDemo.main(SerializationDemo.java:40)

This output reveals a thrown instance of the InvalidClassException class. This exception object was thrown during deserialization because StringTokenizer does not possess a noargument constructor.

We can overcome this problem by taking advantage of the wrapper class pattern that I presented in Chapter 3. Furthermore, we declare a pair of private methods in the subclass that the serialization and deserialization mechanisms look for and call.

Normally, the serialization mechanism writes out a class's instance fields to the underlying output stream. However, you can prevent this from happening by declaring a private void writeObject(ObjectOutputStream oos) method in that class.

When the serialization mechanism discovers this method, it calls the method instead of automatically outputting instance field values. The only values that are output are those explicitly output via the method.

Conversely, the deserialization mechanism assigns values to a class's instance fields that it reads from the underlying input stream. However, you can prevent this from happening by declaring a private void readObject(ObjectInputStream ois) method.

When the deserialization mechanism discovers this method, it calls the method instead of automatically assigning values to instance fields. The only values that are assigned to instance fields are those explicitly assigned via the method.

Because SerializableStringTokenizer does not introduce any fields, and because StringTokenizer does not offer access to its internal fields, what would a serialized SerializableStringTokenizer object include?

Although we cannot serialize StringTokenizer's internal state, we can serialize the argument(s) passed to its constructors, such as the string being tokenized. The deserialized StringTokenizer object is then primed to being tokenizing.

Listing 10-22 reveals the refactored SerializableStringTokenizer and SerializationDemo classes.

Listing 10-22. Solving problematic deserialization import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable;

import java.util.StringTokenizer;

class SerializableStringTokenizer implements Serializable {

private StringTokenizer st; private String str, delim; private boolean returnDelims;

SerializableStringTokenizer(String str) {

SerializableStringTokenizer(String str, String delim) {

this(str, delim, false);

SerializableStringTokenizer(String str, String delim, boolean returnDelims)

this.delim = delim;

this.returnDelims = returnDelims;

st = new StringTokenizer(str, delim, returnDelims);

private void writeObject(ObjectOutputStream oos) throws IOException {

oos.writeUTF(str);

oos.writeUTF(delim);

oos.writeBoolean(returnDelims);

private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException

returnDelims = ois.readBoolean();

st = new StringTokenizer(str, delim, returnDelims);

public int countTokens() {

return st.countTokens();

public String nextToken() {

return st.nextToken();

public class SerializationDemo {

public static void main(String[] args) {

SerializableStringTokenizer sst;

sst = new SerializableStringTokenizer("A,B,C,D", ",", true); System.out.println("Number of tokens = " + sst.countTokens()); System.out.println("First token = " + sst.nextToken()); System.out.println("Second token = " + sst.nextToken());

ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(sst); oos.close();

System.out.println("sst object written to byte array"); ByteArrayInputStream bais;

bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); sst = (SerializableStringTokenizer) ois.readObject(); System.out.println("sst object read from byte array"); System.out.println("Number of tokens = " + sst.countTokens()); System.out.println("First token = " + sst.nextToken()); System.out.println("Second token = " + sst.nextToken());

ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(sst); oos.close();

System.out.println("sst object written to byte array"); ByteArrayInputStream bais;

bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); sst = (SerializableStringTokenizer) ois.readObject(); System.out.println("sst object read from byte array"); System.out.println("Number of tokens = " + sst.countTokens()); System.out.println("First token = " + sst.nextToken()); System.out.println("Second token = " + sst.nextToken());

catch (Exception e) {

e.printStackTrace();

SerializableStringTokenizer's writeObject(ObjectOutputStream) and readObject(ObjectInputStream) methods rely on DataOutput and DataInput methods: they do not need to call writeObject() and readObject() to perform their tasks.

When you run this application, it generates the following output, which reveals that the deserialized SerializableStringTokenizer object is ready to extract tokens:

Number of tokens = 7 First token = A Second token = , sst object written to byte array sst object read from byte array Number of tokens = 7 First token = A Second token = ,

The writeObject(ObjectOutputStream) and readObject(ObjectInputStream) methods can be used to serialize/deserialize data items beyond the normal state (non-transient instance fields); for example, serializing/deserializing the contents of a static field.

However, before serializing or deserializing the additional data items, you must tell the serialization and deserialization mechanisms to serialize or deserialize the object's normal state. The following methods help you accomplish this task:

■ ObjectOutputStream's defaultWriteObject() method outputs the object's normal state. Your writeObject(ObjectOutputStream) method first calls this method to output that state, and then outputs additional data items via ObjectOutputStream methods such as writeUTF().

■ ObjectInputStream's defaultReadObject() method inputs the object's normal state. Your readObject(ObjectInputStream) method first calls this method to input that state, and then inputs additional data items via ObjectInputStream methods such as readUTF().

Externalization

In addition to default serialization/deserialization and custom serialization/deserialization, Java supports externalization. Unlike default/custom serialization/deserialization, externalization offers complete control over the serialization and deserialization tasks.

NOTE: Externalization helps you improve the performance of the reflection-based serialization and deserialization mechanisms by giving you complete control over what fields are serialized and deserialized.

Java supports externalization via its Externalizable interface. This interface declares the following pair of public methods:

■ void writeExternal(ObjectOutput out) saves the calling object's contents by calling various methods on the out object. This method throws IOException when an I/O error occurs. (ObjectOutput is a subinterface of DataOutput and is implemented by ObjectOutputStream.)

■ void readExternal(ObjectInput in) restores the calling object's contents by calling various methods on the in object. This method throws IOException when an I/O error occurs, and ClassNotFoundException when the class of the object being restored cannot be found. (ObjectInput is a subinterface of DataInput and is implemented by ObjectInputStream.)

If a class implements Externalizable, its writeExternal() method is responsible for saving all field values that are to be saved. Also, its readExternal() method is responsible for restoring all saved field values and in the order they were saved.

Listing 10-23 presents a refactored version of Listing 10-19's Employee class to show you how to take advantage of externalization.

Listing 10-23. Refactoring Listing 10-19's Employee class to support externalization import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput;

public class Employee implements Externalizable {

private String name;

private int age;

public Employee()

System.out.println("Employee() called");

public Employee(String name, int age)

this.name = name;

public String getName() { return name; } public int getAge() { return age; } ^Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

System.out.println("readExternal() called"); name = in.readUTF(); age = in.readInt();

^Override public void writeExternal(ObjectOutput out) throws IOException

System.out.println("writeExternal() called");

out.writeUTF(name);

out.writeInt(age);

Employee declares a public Employee() constructor because each class that participates in externalization must declare a public noargument constructor. This constructor is called during deserialization to instantiate the object.

CAUTION: The deserialization mechanism throws InvalidClassException with a "no valid constructor" message when it does not detect a public noargument constructor.

You initiate externalization by instantiating ObjectOutputStream and calling its writeObject(Object) method, or by instantiating ObjectInputStream and calling its readObject() method.

NOTE: When passing an object whose class (directly/indirectly) implements Externalizable to writeObject(), the writeObject()-initiated serialization mechanism writes only the identity of the object's class to the object output stream.

Suppose you compiled Listing 10-20's SerializationDemo.java source code and Listing 10-23's Employee.java source code in the same directory. Now suppose you executed java SerializationDemo. In response, you would observe the following output:

writeExternal() called Employee() called readExternal() called John Doe 36

Before serializing an object, the serialization mechanism checks the object's class to see if it implements Externalizable. If so, the mechanism calls writeExternal(). Otherwise, it looks for a private writeObject(ObjectOutputStream) method, and calls this method if present. If this method is not present, the mechanism performs default serialization, which includes only non-transient instance fields.

Before deserializing an object, the deserialization mechanism checks the object's class to see if it implements Externalizable. If so, the mechanism attempts to instantiate the class via the public noargument constructor. Assuming success, it calls readExternal().

If the object's class does not implement Externalizable, the deserialization mechanism looks for a private readObject(ObjectlnputStream) method. If this method is not present, the mechanism performs default deserialization, which includes only non-transient instance fields.

Was this article helpful?

+1 0

Post a comment