10.2 Custom Serialization
Not every piece of program state can, or
should be, serialized. Such things as
FileDescriptor objects are inherently
platform-specific or virtual machine-dependent. If a
FileDescriptor were serialized, for example, it
would have no meaning when deserialized in a different virtual
machine. For this reason, and also for the important security reasons
described earlier, not all objects can be serialized.Even
when an object is serializable, it may not make sense for it to
serialize all its state. Some fields may be
"scratch" fields that can hold
temporary or precomputed values but don't actually
hold state needed to restore a serialized object. Consider a GUI
component. It may define fields that store the coordinates of the
last mouse click it received. This information is never of interest
when the component is deserialized, so there's no
need to bother saving the values of these fields as part of the
component's state. To tell the serialization
mechanism that a field shouldn't be saved, simply
declare it transient:
protected transient short last_x, last_y;
// Temporary fields for mouse pos
There are also situations where a
field is not transient (i.e., it does contain an important part of an
object's state), but for some reason it
can't be successfully serialized. Consider another
GUI component that computes its preferred size based on the size of
the text it displays. Because fonts have slight size variations from
platform to platform, this precomputed preferred size
isn't valid if the component is serialized on one
type of platform and deserialized on another. Since the preferred
size fields will not be reliable when deserialized, they should be
declared transient so that they
don't take up space in the serialized object. But in
this case, their values must be recomputed when the object is
deserialized.A class can define custom serialization
and deserialization behavior (such as recomputing a preferred size)
for its objects by implementing writeObject( ) and
readObject( ) methods. Surprisingly, these methods
are not defined by any interface, and they must be declared
private. If a class defines these methods, the
appropriate one is invoked by the
ObjectOutputStream or
ObjectInputStream when an object is serialized or
deserialized.For example, a GUI component
might define a readObject( ) method to give it an
opportunity to recompute its preferred size upon deserialization. The
method might look like this:
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject( );// Deserialize the component in the usual way.
this.computePreferredSize( ); // But then go recompute its size.
}
This method calls the defaultReadObject( ) method
of the ObjectInputStream to deserialize the object
as normal and then takes care of the postprocessing it needs to
perform.Example 10-2 is a more complete example of custom
serialization. It shows a class that implements a growable array of
integers, like the IntList class of Example 2-7. This version of the class is declared
Serializable and defines a writeObject(
) method to do some preprocessing before being serialized
and a readObject( ) method to do postprocessing
after deserialization. Note that the size field is
declared transient. The example also overrides
equals( ) so that serialization can be tested, and
overrides hashCode( ) to match.
Example 10-2. SerialIntList.java
package com.davidflanagan.examples.serialization;
import java.io.*;
/**
* A simple class that implements a growable array of ints, and knows
* how to serialize itself as efficiently as a nongrowable array.
**/
public class SerialIntList implements Serializable {
// These are the fields of this class. By default the serialization
// mechanism would just write them out. But we've declared size to be
// transient, which means it will not be serialized. And we've
// provided writeObject( ) and readObject( ) methods below to customize
// the serialization process.
protected int[ ] data = new int[8]; // An array to store the numbers.
protected transient int size = 0;
// Index of next unused element of array
/** Return an element of the array */
public int get(int index) {
if (index >= size) throw new ArrayIndexOutOfBoundsException(index);
else return data[index];
}
/** Add an int to the array, growing the array if necessary */
public void add(int x) {
if (data.length==size) resize(data.length*2); // Grow array if needed.
data[size++] = x; // Store the int in it.
}
/** An internal method to change the allocated size of the array */
protected void resize(int newsize) {
int[ ] newdata = new int[newsize]; // Create a new array
System.arraycopy(data, 0, newdata, 0, size); // Copy array elements.
data = newdata; // Replace old array
}
/**
* Get rid of unused array elements before serializing the array. This
* may reduce the number of array elements to serialize. It also makes
* data.length == size, so there is no need to safe the (transient) size
* field. The serialization mechanism will automatically call this method
* when serializing an object of this class. Note that this must be
* declared private.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
if (data.length > size) resize(size); // Compact the array.
out.defaultWriteObject( ); // Then write it out normally.
}
/**
* Restore the transient size field after deserializing the array.
* The serialization mechanism automatically calls this method.
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject( ); // Read the array normally.
size = data.length; // Restore the transient field.
}
/**
* Does this object contain the same values as the object o?
* We override this Object method so we can test the class.
**/
public boolean equals(Object o) {
if (!(o instanceof IntList)) return false;
IntList that = (IntList) o;
if (this.size != that.size) return false;
for(int i = 0; i < this.size; i++)
if (this.data[i] != that.data[i]) return false;
return true;
}
/** We must override this method when we override equals( ). */
public int hashCode( ) {
int code = 1; // non-zero to hash [0] and [ ] to distinct values
for(int i = 0; i < size; i++)
code = code*997 + data[i]; // ignore overflow
return code;
}
/** A main( ) method to prove that it works */
public static void main(String[ ] args) throws Exception {
IntList list = new IntList( );
for(int i = 0; i < 100; i++) list.add((int)(Math.random( )*40000));
IntList copy = (IntList)Serializer.deepclone(list);
if (list.equals(copy)) System.out.println("equal copies");
Serializer.store(list, new File("intlist.ser"));
}
}