Java Serialization Code Examples
Evolving Classes when three are three machines invloved A <---> B <---> C
When java deserialize object, any field of the object that does not appear in the stream is set to its default value. Values that appear in the stream, but not in the object, are discarded.
This works fine in most cases.
But how about there are three machines when serialize and deserialize?
For example, we have a class User, and first in machine A we serialize it and transfer it to machine B, in machine B, we deserialize it and do do some operations then serialize it and transfer it to machine C. in machine C, we deserialize it and do whatever we want.
We will also need to return the object back, C to B, then to A.
A(User) <---> B(User) <---> C(User)
Now, we have to add some new fields - for instance property age - to the Class User.
Imagine that the class version on A is latest, on B old version, on C is newest.
On B, when it deserialize User, and it would discard age property in the stream, then B deserialize it and transfer to C, there would be no field age, so on machine C, Java set age field to default value 0.
In this process, the age value is missing due to the old version on B.
One solution to this problem is to save all fields of the logical state of the
object User in a map, then writeObject writes this map to ObjectOutputStream, readObject get every field value from this map.
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Map fieldsMap = new HashMap();
private void writeObject(ObjectOutputStream out) throws IOException {
fieldsMap.put("firstName", firstName);
fieldsMap.put("age", age);
out.writeUnshared(fieldsMap);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
fieldsMap = (HashMap) in.readObject();
firstName = (String) fieldsMap.remove("firstName");
age = (Integer) fieldsMap.remove("age");
}
}
Then if in B, its class definition doesn't include property age, its class looks like below:
public class User implements Serializable {
private Map fieldsMap = new HashMap();
private void writeObject(ObjectOutputStream out) throws IOException {
fieldsMap.put("firstName", firstName);
out.writeUnshared(fieldsMap);
}
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException {
fieldsMap = (HashMap) in.readObject();
firstName = (String) fieldsMap.remove("firstName");
}
}
Then in B, it doesn't modify age property in fieldsMap, so when deserialize this object,age property is still in fieldsMap, thus C can restore the original age value.
Changing type of an instance field
For example, we have a Class Inventory:
public class Inventory implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int amount;
}
Now we have to promote amount field from int to long in Class Inventory,
and meanwhile we need to ensure class compatibility, that is we must guarantee the new version
of class can deserialize an object saved by the earlier version of the class, and vice verse.
If we simply convert the int type to long, then when use the new version of class to deserialize an object saved by the earlier version, or vice verse, it would throw the following exception:
java.io.InvalidClassException: io.Inventory; incompatible types for field amount
We can use serialPersistentFields, readObject, writeObject to maintain backward compatibility with the serialization format.
public class Inventory implements Serializable {private static final long serialVersionUID = 1L;
private String name;
private long longAmount;
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("name", String.class),
new ObjectStreamField("amount", int.class),
new ObjectStreamField("longAmount", long.class), };
private void writeObject(ObjectOutputStream out) throws IOException {
ObjectOutputStream.PutField fields = out.putFields();
fields.put("name", name);
int intAmount;
if (longAmount > Integer.MAX_VALUE)
intAmount = Integer.MAX_VALUE;
else
intAmount = (int) longAmount;
// or for short
// intAmount = (int) ((longAmount > Integer.MAX_VALUE)? Integer.MAX_VALUE:longAmount);
// For compatibility reason, we need to put the old values first.
fields.put("amount", intAmount);
fields.put("longAmount", longAmount);
out.writeFields();
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
ObjectInputStream.GetField fields = in.readFields();
name = (String) fields.get("name", "");
// it there is no longAmount in the stream, then it is serialized by
// older version, and use the int type amount
if (fields.defaulted("longAmount")) {
longAmount = fields.get("amount", (int) 0);
} else {
// long type - longAmount exists in this stream
// the "(long)0" is a must, it tells java to read a field
// named longAmount with a value of type long
longAmount = fields.get("longAmount", (long) 0);
}
// or for short
// longAmount = fields.defaulted("longAmount") ?
// fields.get("amount",(int) 0) : fields.get("longAmount", (long) 0);
}
We have to run various kinds of tests to ensure class compatibility, first guarantee serialize and deserialize works when in same version, and then serialize an instance in the new release and deserialize it in old releases, and vice versa.
Distinguish default value from no-such-field case
After add a field to existing class, when deserialize, we need to distinguish the case the response object doesn't include this field at all from that the field has default value.
In the new class, directly setting the field to invalid value would not work, as when deserialize, If the stream doesn't include one field, Java would directly set the object to its default value, object to null, int to 0, etc.
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String firstName;
// age field is added later, and need to distinguish its default value 0 from case response doesn't include this field
// directly assigning age -1 would not work, when deserialize,
// the stream doesn't include one field, Java would set the object to its default value, object to null, int to 0, etc.
private int age;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = in.readFields();
firstName = (String) fields.get("firstName","");
age = fields.defaulted("age") ?
-1 : fields.get("age", (int) 0);
}
// we still need rewrite writeObject
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
}
Miscs
public class SerializationUtils {
public static Object deserialize(String filename)
throws FileNotFoundException, IOException, ClassNotFoundException {
Object object = null;
FileInputStream fis = null;
ObjectInputStream in = null;
fis = new FileInputStream(filename);
in = new ObjectInputStream(fis);
object = in.readObject();
in.close();
return object;
}
public static void serialize(String filename, Object object)
throws FileNotFoundException, IOException {
FileOutputStream fos = null;
ObjectOutputStream out = null;
fos = new FileOutputStream(filename);
out = new ObjectOutputStream(fos);
out.writeObject(object);
out.flush();
out.close();
}
}
Read/Write object with ByteArrayInputStream/ByteArrayOutputStream
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(bout);
oout.writeInt(20);
oout.flush();
byte[] bytes = bout.toByteArray();
ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bin);
System.out.println(oin.readInt());
Resources:
Java I/O 2nd edition - Chapter 13. Object Serialization
Effective Java (2nd Edition) - Chapter 11 Serialization
Java Object Serialization Specification