Serialization and Versioning
Class versioning comes into play when we serialize an object with one definition of a class, but deserialize the streamed object with a different class definition. By streamed object, we mean the serialized representation of an object. Between serialization and deserialization of an object, the class definition can change.
Note that at serialization and at deserialization, the definition of the class (i.e., its bytecode file) should be accessible. In the examples so far, the class definition has been the same at both serialization and deserialization. Example 20.10 illustrates the problem of class definition mismatch at deserialization and the solution provided by Java.
Example 20.10 makes use of the following classes (numbering refers to code lines in the example):
(1) The original version of the serializable class Item. It has one field named price. An object of this class will be serialized and read using different versions of this class.
(2) A newer version of the class Item that has been augmented with a field for the weight of an item. This class will only be used for deserialization of objects that have been serialized with the original version of the class.
(3) The class Serializer serializes an object of the original version of the class Item.
(4) The class DeSerializer deserializes a streamed object of the class Item. In the example, deserialization is based on a different version of the Item class than the one used at serialization.
There are no surprises if we use the original class Item to serialize and deserialize an object of the Item class.
// Original version of the Item class.
public class Item implements Serializable { // (1)
private double price;
//…
}
Result:
Before writing: Price: 100.00
After reading: Price: 100.00
If we deserialize a streamed object of the original class Item at (1) based on the byte-code file of the augmented version of the Item class at (2), an InvalidClassException is thrown at runtime.
// New version of the Item class.
public class Item implements Serializable { // (2)
private double price;
private double weight; // Additional field
//…
}
Result (edited to fit on the page):
Exception in thread “main” java.io.InvalidClassException: Item;
local class incompatible:
stream classdesc serialVersionUID = -4194294879924868414,
local class serialVersionUID = -1186964199368835959
…
at DeSerializer.main(DeSerializer.java:14)
The question is, how was the class definition mismatch discovered at runtime? The answer lies in the stack trace of the exception thrown. The local class was incompatible, meaning the class we are using to deserialize is not compatible with the class that was used when the object was serialized. In addition, two long numbers are printed, representing the serialVersionUID of the respective class definitions. The first serialVersionUID is generated by the serialization process based on the class definition and becomes part of the streamed object. The second serialVersionUID is generated based on the local class definition that is accessible at deserialization. The two are not equal, and deserialization fails.
A serializable class can provide its serialVersionUID by declaring it in its class declaration, exactly as shown below, except for the initial value which of course can be different:
static final long serialVersionUID = 100L; // Appropriate value.
As we saw in the example above, if a serializable class does not provide a serialVersionUID, one is implicitly generated. By providing an explicit serialVersionUID, it is possible to control what happens at deserialization. As newer versions of the class are created, the serialVersionUID can be kept the same until it is deemed that older streamed objects are no longer compatible for deserialization. After the change to the serialVersionUID, it will not be possible to deserialize older streamed objects of the class based on newer versions of the class. Although static members of a class are not serialized, the only exception is the value of the static final long serialVersionUID field.
In the scenario below, the original version and the newer version of the class Item both declare a serialVersionUID at (1a) and at (2a), respectively, that has the same value. An Item object is serialized using the original version, but deserialized based on the newer version. We see that serialization succeeds, and the weight field is initialized to the default value 0.0. In other words, the object created is of the newer version of the class.
// Original version of the Item class.
public class Item implements Serializable { // (1)
static final long serialVersionUID = 1000L; // (1a)
private double price;
//…
}
// New version of the Item class.
public class Item implements Serializable { // (2)
static final long serialVersionUID = 1000L; // (2a) Same serialVersionUID
private double price;
private double weight; // Additional field
//…
}
Result:
Before writing: Price: 100.00
After reading: Price: 100.00, Weight: 0.00
However, if we now deserialize the streamed object of the original class having 1000L as the serialVersionUID, based on the newer version of the class having the serialVersionUID equal to 1001L, deserialization fails as we would expect because the serialVersionUIDs are different.
// New version of the Item class.
public class Item implements Serializable { // (2)
static final long serialVersionUID = 1001L; // (2b) Different serialVersionUID
private double price;
private double weight;
//…
}
Result (edited to fit on the page):
Exception in thread “main” java.io.InvalidClassException: Item;
local class incompatible:
stream classdesc serialVersionUID = 1000,
local class serialVersionUID = 1001
…
at DeSerializer.main(DeSerializer.java:14)
Best practices advocate that serializable classes should use the serialVersionUID solution for better control of what happens at deserialization as classes evolve.
Example 20.10 Class Versioning
import java.io.Serializable;
// Original version of the Item class.
public class Item implements Serializable { // (1)
//static final long serialVersionUID = 1000L; // (1a)
private double price;
public Item(double price) {
this.price = price;
}
@Override
public String toString() {
return String.format(“Price: %.2f%n”, this.price);
}
}
import java.io.Serializable;
// New version of the Item class.
public class Item implements Serializable { // (2)
//static final long serialVersionUID = 1000L; // (2a)
//static final long serialVersionUID = 1001L; // (2b)
private double price;
private double weight;
public Item(double price, double weight) {
this.price = price;
this.weight = weight;
}
@Override
public String toString() {
return String.format(“Price: %.2f, Weight: %.2f”, this.price, this.weight);
}
}
// Serializer for objects of class Item.
import java.io.*;
public class Serializer { // (3)
public static void main(String args[])
throws IOException, ClassNotFoundException {
try (// Set up the output stream:
FileOutputStream outputFile = new FileOutputStream(“item_storage.dat”);
ObjectOutputStream outputStream = new ObjectOutputStream(outputFile)) {
// Serialize an object of the original class:
Item item = new Item(100.00);
System.out.println(“Before writing: ” + item);
outputStream.writeObject(item);
}
}
}
// Deserializer for objects of class Item.
import java.io.*;
public class DeSerializer{ // (4)
public static void main(String args[])
throws IOException, ClassNotFoundException {
try (// Set up the input streams:
FileInputStream inputFile = new FileInputStream(“item_storage.dat”);
ObjectInputStream inputStream = new ObjectInputStream(inputFile)) {
// Read a serialized object of the Item class.
Item item = (Item) inputStream.readObject();
// Write data on standard output stream.
System.out.println(“After reading: ” + item);
}
}
}