Here are a set of rules I find useful to follow when serializing java classes that may be persisted and later require versioning.
I enforce all the following rules in test code that uses reflection to walk recursively through all members of serialized objects, and checks for the required members and modifiers.
serialVersionUID
with a value of 1.
Increment to a new value only when you have permanently broken
the backward compatibility of previously persisted versions.
Our goal is never to break compatibility.
private static final long serialVersionUID = 1L; |
writeObject
and readObject
for every class
in your hierarchy, with these exact signatures:
private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException |
transient
to ensure
that you explicitly serialize everything.
ObjectOutputStream#defaultWriteObject
or
ObjectInputStream#defaultReadObject
from writeObject
or
readObject
. We will not use any default serialization.
version
as an integer to mark a change in
what is serialized, but still supported. (For example, an int
is replaced by a long. A new member can be given a good
default value.)
release
as an integer shared by
other objects that are serialized together. This can be useful
for adjusting to global structural changes. Use a version
for
changes withing a single class, and a release
number for
changes that involve multiple classes. Maintain the release
integer externally so that it can be shared. Serializable objects
might depend on each other and need to change as a group.
It is also possible your serialization depends on a third party.
I try very hard to avoid this situation. I might use a change
in release just to log a warning.
Map
with keys
identical to the names of the member variables, and serialize
the Map
as a single object. This makes it much easier to
detect the addition, removal, and modification of members
without remembering the order they were previously written.
public class Foo implements java.io.Serializable { private static final long serialVersionUID = 1L; // try never to change private static final int VERSION = 2; private transient double[] data = {3.14159, 2.71828}; // ... private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { java.util.Map<String, Object> map = new java.util.HashMap<String, Object>(); map.put("data", data); map.put("VERSION", VERSION); out.writeObject(map); } private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { @SuppressWarnings("unchecked") java.util.Map<String, Object> map = (java.util.Map<String, Object>) in.readObject(); int version = (Integer) map.get("VERSION"); if (version == VERSION) { data = (double[]) map.get("data"); if (version > VERSION) { throw new IOException("Cannot deserialize data from version "+version+" of this code, "+ "which is newer than current version "+VERSION); } else { // convert older version, previously serialized as floats float[] olderData = (float[]) map.get("data"); data = new double[olderData.length]; for (int i=0; i<data.length; ++i) {data[i] = olderData[i];} } } } |
Foo$1
, etc. These compiled names are not under your control.
There are a few special cases.
ObjectOutputStream.PutField
and
ObjectInputStream.GetField
instead of a Map.
Unfortunately, you can only add actual non-transient fields to
the PutFields
buffer. You lose the ability to convert or
simplify those fields for backward-compatibility. You cannot
pass along a version number that is not a field. GetField
also cannot list the available fields. These buffers do not
even add much type safety because the get
and set
methods are overloaded by type in an unusual, error-prone way.
Map
for very small objects.
If you look at the implementation of HashMap#writeObject
you will see that it does not add much overhead for a
moderately sized object. It serializes the keys and their
values and a few additional integers.
Bill Harlan, December 2009
Return to parent directory.