Using writeObject and readObject
- Details
- Category: Programming Guides & Tutorials
- Published on Saturday, 09 February 2013 04:28
- Written by Vinayaga Moorthy
Consider the problem: we have a Dog object we want to save. The Dog has a Collar, and the Collar has state that should also be saved as part of the Dog's state. But…the Collar is not Serializable, so we must mark it transient. That means when the Dog is deserialized, it comes back with a null Collar. What can we do to somehow make sure that when the Dog is deserialized, it gets a new Collar that matches the one the Dog had when the Dog was saved?
Java serialization has a special mechanism just for this—a set of private methods you can implement in your class that, if present, will be invoked automatically during serialization and deserialization. It's almost as if the methods were defined in the Serializable interface, except they aren't. They are part of a special callback contract the serialization system offers you that basically says, "If you (the programmer) have a pair of methods matching this exact signature (you'll see them in a moment), these methods will be called during the serialization/deserialization process.
These methods let you step into the middle of serialization and deserialization. So they're perfect for letting you solve the Dog/Collar problem: when a Dog is being saved, you can step into the middle of serialization and say, "By the way, I'd like to add the state of the Collar's variable (an int) to the stream when the Dog is serialized." You've manually added the state of the Collar to the Dog's serialized representation, even though the Collar itself is not saved.
Of course, you'll need to restore the Collar during deserialization by stepping into the middle and saying, "I'll read that extra int I saved to the Dog stream, and use it to create a new Collar, and then assign that new Collar to the Dog that's being deserialized." The two special methods you define must have signatures that look EXACTLY like this:
private void writeObject(ObjectOutputStream os) {
// your code for saving the Collar variables
}
private void readObject(ObjectInputStream os) {
// your code to read the Collar state, create a new Collar,
// and assign it to the Dog
}
Yes, we're going to write methods that have the same name as the ones we've been calling! Where do these methods go? Let's change the Dog class:
class Dog implements Serializable {
transient private Collar theCollar; // we can't serialize this
private int dogSize;
public Dog(Collar collar, int size) {
theCollar = collar;
dogSize = size;
}
public Collar getCollar() { return theCollar; }
private void writeObject(ObjectOutputStream os) {
// throws IOException { // 1
try {
os.defaultWriteObject(); // 2
os.writeInt(theCollar.getCollarSize()); // 3
} catch (Exception e) { e.printStackTrace(); }
}
private void readObject(ObjectInputStream is) {
// throws IOException, ClassNotFoundException { // 4
try {
is.defaultReadObject(); // 5
theCollar = new Collar(is.readInt()); // 6
} catch (Exception e) { e.printStackTrace(); }
}
}
Let's take a look at the preceding code.
In our scenario we've agreed that, for whatever real-world reason, we can't serialize a Collar object, but we want to serialize a Dog. To do this we're going to implement writeObject() and readObject(). By implementing these two methods you're saying to the compiler: "If anyone invokes writeObject() or readObject() concerning a Dog object, use this code as part of the read and write".
1. Like most I/O-related methods writeObject() can throw exceptions. You can declare them or handle them but we recommend handling them.
2. When you invoke defaultWriteObject() from within writeObject() you're telling the JVM to do the normal serialization process for this object. When implementing writeObject(), you will typically request the normal serialization process, and do some custom writing and reading too.
3. In this case we decided to write an extra int (the collar size) to the stream that's creating the serialized Dog. You can write extra stuff before and/or after you invoke defaultWriteObject(). BUT…when you read it back in, you have to read the extra stuff in the same order you wrote it.
4. Again, we chose to handle rather than declare the exceptions.
5. When it's time to deserialize, defaultReadObject() handles the normal deserialization you'd get if you didn't implement a readObject() method.
6. Finally we build a new Collar object for the Dog using the collar size that we manually serialized. (We had to invoke readInt() after we invoked defaultReadObject() or the streamed data would be out of sync!)
Remember, the most common reason to implement writeObject() and readObject() is when you have to save some part of an object's state manually. If you choose, you can write and read ALL of the state yourself, but that's very rare. So, when you want to do only a part of the serialization/deserialization yourself, you MUST invoke the defaultReadObject() and defaultWriteObject() methods to do the rest.
Which brings up another question—why wouldn't all Java classes be serializable? Why isn't class Object serializable? There are some things in Java that simply cannot be serialized because they are runtime specific. Things like streams, threads, runtime, etc. and even some GUI classes (which are connected to the underlying OS) cannot be serialized.
blog comments powered by Disqus
More 

