Customizing Object Serialization
As we have seen, the class of the object must implement the Serializable interface if we want the object to be serialized. If this object is a compound object, then all its constituent objects must also be serializable, and so on.
It is not always possible for a client to declare that a class is Serializable. It might be declared final, and therefore not extendable. The client might not have access to the code, or extending this class with a serializable subclass might not be an option. Java provides a customizable solution for serializing objects in such cases.
Customized serialization discussed here is not applicable to record classes.
The basic idea behind the scheme is to use default serialization as much as possible, and to provide hooks in the code for the serialization mechanism to call specific methods to deal with objects or values that should not or cannot be serialized by the default methods of the object streams.
Customizing serialization is illustrated in Example 20.8, using the Wheel and Unicycle classes from Example 20.7. The serializable class Unicycle would like to use the Wheel class, but this class is not serializable. If the wheel field in the Unicycle class is declared to be transient, it will be ignored by the default serialization procedure. This is not a viable option, as the unicycle will be missing the wheel size when a serialized unicycle is deserialized, as was illustrated in Example 20.7.
Any serializable object has the option of customizing its own serialization if it implements the following pair of methods:
private void writeObject(ObjectOutputStream) throws IOException;
private void readObject(ObjectInputStream)
throws IOException, ClassNotFoundException;
These methods are not part of any interface. Although private, these methods can be called by the JVM. The first method above is called on the object when its serialization starts. The serialization procedure uses the reference value of the object to be serialized that is passed in the call to the ObjectOutputStream.writeObject() method, which in turn calls the first method above on this object. The second method above is called on the object created when the deserialization procedure is initiated by the call to the ObjectInputStream.readObject() method.
Customizing serialization for objects of the class Unicycle in Example 20.8 is achieved by the private methods at (3c) and (3d). Note that the field wheel is declared transient at (3b) and excluded by the normal serialization process.
In the private method writeObject() at (3c) in Example 20.8, the pertinent lines of code are the following:
oos.defaultWriteObject(); // Method in the ObjectOutputStream class
oos.writeInt(wheel.getWheelSize()); // Method in the ObjectOutputStream class
The call to the defaultWriteObject() method of the ObjectOutputStream does what its name implies: normal serialization of the current object. The second line of code does the customization: It writes the binary int value of the wheel size to the ObjectOutputStream. The code for customization can be called both before and after the call to the defaultWriteObject() method, as long as the same order is used during deserialization.
In the private method readObject() at (3d), the pertinent lines of code are the following:
ois.defaultReadObject(); // Method in the ObjectInputStream class
int wheelSize = ois.readInt(); // Method in the ObjectInputStream class
this.wheel = new Wheel(wheelSize);
The call to the defaultReadObject() method of the ObjectInputStream does what its name implies: normal deserialization of the current object. The second line of code reads the binary int value of the wheel size from the ObjectInputStream. The third line of code creates a Wheel object, passes this value in the constructor call, and assigns its reference value to the wheel field of the current object. Again, code for customization can be called both before and after the call to the defaultReadObject() method, as long as it is in correspondence with the customization code in the writeObject() method.
The client class SerialClient in Example 20.8 is the same as the one in Example 20.7. The output from the program confirms that the object state prior to serialization is identical to the object state after deserialization.
Example 20.8 Customized Serialization
public class Wheel { // (1b)
private int wheelSize;
public Wheel(int ws) { wheelSize = ws; }
public int getWheelSize() { return wheelSize; }
@Override
public String toString() { return “wheel size: ” + wheelSize; }
}
Click here to view code image
import java.io.*;
public class Unicycle implements Serializable { // (2)
transient private Wheel wheel; // (3b)
public Unicycle(Wheel wheel) { this.wheel = wheel; }
@Override
public String toString() { return “Unicycle with ” + wheel; }
private void writeObject(ObjectOutputStream oos) { // (3c)
try {
oos.defaultWriteObject();
oos.writeInt(wheel.getWheelSize());
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream ois) { // (3d)
try {
ois.defaultReadObject();
int wheelSize = ois.readInt();
this.wheel = new Wheel(wheelSize);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public class SerialClient { // Same as in
Example 20.7
}
Output from the program:
Before writing: Unicycle with wheel size: 65
After reading: Unicycle with wheel size: 65