Versioning
When an object is written onto a stream, only the state of the object and the name of the object's class are stored; the byte codes for the object's class are not stored with the object. There's no guarantee that a serialized object will be deserialized into the same environment from which it was serialized. It's possible for the class definition to change between the time the object is written and the time it's read.
There are even more differences when methods, constructors, and static and transient fields are considered. Not all changes, however, prevent deserialization. For instance, the values of static fields aren't saved when an object is serialized. Therefore, you don't have to worry about adding or deleting a static field to or from a class. Similarly, serialization completely ignores the methods in a class, so changing method bodies or adding or removing methods does not affect serialization.
However, removing an instance field does affect serialization because deserializing an object saved by the earlier version of the class results in an attempt to set the value of a field that no longer exists.
13.6.1. Compatible and Incompatible Changes
Changes to a class are divided into two groups: compatible changes and incompatible changes. Compatible changes are those that do not affect the serialization format of the object, like adding a method or deleting a static field. Incompatible changes are those that do prevent a previously serialized object from being restored. Examples include changing a class's superclass or changing the type of a field. As a general rule, any change that affects the signature of the class itself or its nontransient instance fields of a class is incompatible while any change that does not affect the signatures of the nontransient instance fields of a class is compatible. However, there are a couple of exceptions. The following changes are compatible:
- Most changes to constructors and methods, whether instance or static. Serialization doesn't touch the methods of a class. The exceptions are those methods directly involved in the serialization process, particularly writeObject( ) and readObject( ).
- All changes to static fieldschanging their type, their names, adding or removing them, etc. Serialization ignores all static fields.
- All changes to transient fieldschanging their type, their names, adding or removing them, etc. Serialization ignores all transient fields.
- Adding or removing an interface (except the Serializable interface) from a class. Interfaces say nothing about the instance fields of a class.
- Adding or removing inner classes.
- Changing the access specifiers of a field. Serialization does not respect access protection.
- Changing a field from static to nonstatic or transient to nontransient. This is the same as adding a field.
The following changes are incompatible and thus prevent deserialization of serialized objects:
- Changing the name of a class.
- Changing the type of an instance field.
- Changing the superclass of a class. This may affect the inherited state of an object.
- Changing the writeObject( ) or readObject( ) method (discussed later) in an incompatible fashion.
- Changing a class from Serializable to Externalizable (discussed later) or Externalizable to Serializable.
Finally, adding, removing, or changing the name of a nontransient instance field is incompatible by default. However, it can usually be made compatible with a small effort and an SUID.
13.6.2. SUIDs
To help identify compatible or incompatible classes, each serializable class has a stream unique identifier, SUID for short. When Java deserializes an object, it compares the SUID of the class found in the stream to the SUID of the class with the same name in the local classpath. If they match, Java assumes the two versions of the class are compatible. If they don't match, Java assumes the class has changed in an incompatible way since the object was serialized and throws a java.io.InvalidClassException:
Exception in thread "main" java.io.InvalidClassException: Test; local class incompatible: stream classdesc serialVersionUID = 5590355372728923878, local class serialVersionUID = -1390649424173445192
By default, the SUID is calculated by hashing together all the pieces of a class's interface: the signature of the class, the signatures of the nonprivate methods in the class, the signatures of the fields, and so on. If any of these change, the SUID changes. By default, this is fairly strict. Even compatible changes that don't affect the serialized format such as adding a public method can prevent a serialized object from being deserialized against the newer version of the class.
Sometimes a normally incompatible change can be made compatible. For instance, if you add a new int field to a class, it may be OK for deserialization of old instances of that class to just set the field to 0. If you remove a field from a class, it may be OK for deserialization of old instances to ignore the value stored for that field. Java will do this, but only if the SUIDs of the two versions of the class match.
To tell Java that it's OK to ignore removed fields and use default values for added fields, as well as telling it that other changes don't matter, you can specify the SUID for a class rather than allow it to be calculated automatically. The SUID you specify is a private final static long field named serialVersionUID:
public class UnicodeApplet extends Applet { private static final long serialVersionUID = 5913267123532863320L; // ...
As long as you keep the value of this field constant as you evolve the class, Java will serialize and deserialize old saved instances into new versions of the class and vice versa. However, it now becomes your responsibility to make sure that the old and new versions of the class are indeed compatible. For instance, if you change the name of a field, you'll need to write a little code to make sure the value for the old field gets put in the new field when deserializing. You can do this in the readObject( ) and writeObject( ) methods to be discussed shortly. If you can't maintain forward and backward compatibility with the serialization format, you must change the serialVersionUID field to keep Java from deserializing old instances into the new class version and vice versa.
The serialver tool, included with the JDK, calculates an SUID that fits the class. For example:
% serialver UnicodeApplet UnicodeApplet: static final long serialVersionUID = 5913267123532863320L;
There's also a GUI interface available with the -show flag, as shown in Figure 13-1.
Figure 13-1. The serialver GUI
This generates the same hash code Java would calculate if no serialVersionUID field were present. However, unlike the default hash, you can continue using this same value as the class evolves.
You do not have to use the SUID values that serialver calculates. You can use your own version-numbering scheme. The simplest such scheme would be to give the first version of the class SUID 1, the next incompatible version SUID 2, and so forth. Whether you use a custom SUID or let serialver calculate one for you, you are responsible for deciding when a change to a class is compatible with serialization. The serialver tool does not necessarily generate the same SUID for two compatible but different classes.