Object serialization

Bruce Eckel’s Thinking in Java Contents | Prev | Next

A
particularly clever aspect of object serialization is that it not only saves an
image of your object but it also follows all the handles contained in your
object and saves
those
objects,
and follows all the handles in each of those objects, etc. This is sometimes
referred to as the “
web
of objects” that a single object can be connected to, and it includes
arrays of handles to objects as well as member objects. If you had to maintain
your own object serialization scheme, maintaining the code to follow all these
links would be a bit mind–boggling. However, Java object serialization
seems to pull it off flawlessly, no doubt using an optimized algorithm that
traverses the web of objects. The following example tests the serialization
mechanism by making a “worm” of linked objects, each of which has a
link to the next segment in the worm as well as an array of handles to objects
of a different class,
Data:

//: Worm.java
// Demonstrates object serialization in Java 1.1
import java.io.*;
 
class Data implements Serializable {
  private int i;
  Data(int x) { i = x; }
  public String toString() {
    return Integer.toString(i);
  }
}
 
public class Worm implements Serializable {
  // Generate a random int value:
  private static int r() {
    return (int)(Math.random() * 10);
  }
  private Data[] d = {
    new Data(r()), new Data(r()), new Data(r())
  };
  private Worm next;
  private char c;
  // Value of i == number of segments
  Worm(int i, char x) {
    System.out.println(" Worm constructor: " + i);
    c = x;
    if(--i > 0)
      next = new Worm(i, (char)(x + 1));
  }
  Worm() {
    System.out.println("Default constructor");
  }
  public String toString() {
    String s = ":" + c + "(";
    for(int i = 0; i < d.length; i++)
      s += d[i].toString();
    s += ")";
    if(next != null)
      s += next.toString();
    return s;
  }
  public static void main(String[] args) {
    Worm w = new Worm(6, 'a');
    System.out.println("w = " + w);
    try {
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("worm.out"));
      out.writeObject("Worm storage");
      out.writeObject(w);
      out.close(); // Also flushes output
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("worm.out"));
      String s = (String)in.readObject();
      Worm w2 = (Worm)in.readObject();
      System.out.println(s + ", w2 = " + w2);
    } catch(Exception e) {
      e.printStackTrace();
    }
    try {
      ByteArrayOutputStream bout =
        new ByteArrayOutputStream();
      ObjectOutputStream out =
        new ObjectOutputStream(bout);
      out.writeObject("Worm storage");
      out.writeObject(w);
      out.flush();
      ObjectInputStream in =
        new ObjectInputStream(
          new ByteArrayInputStream(
            bout.toByteArray()));
      String s = (String)in.readObject();
      Worm w3 = (Worm)in.readObject();
      System.out.println(s + ", w3 = " + w3);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

To
make things interesting, the array of
Data
objects inside
Worm
are initialized with random numbers. (This way you don’t suspect the
compiler of keeping some kind of meta-information.) Each
Worm
segment is labeled with a
char
that’s automatically generated in the process of recursively generating
the linked list of
Worms.
When you create a
Worm,
you tell the constructor how long you want it to be. To make the
next
handle it calls the
Worm
constructor with a length of one less, etc. The final
next
handle is left as
null,
indicating the end of the
Worm.

The
point of all this was to make something reasonably complex that couldn’t
easily be serialized. The act of serializing, however, is quite simple. Once the
ObjectOutputStream
is created from some other stream,
writeObject( )
serializes the object. Notice the call to
writeObject( )
for a
String,
as well. You can also write all the primitive data types using the same methods
as
DataOutputStream
(they share the same interface).

There
are two separate
try
blocks that look similar. The first writes and reads a file and the second, for
variety, writes and reads a
ByteArray.
You can read and write an object using serialization to any
DataInputStream
or
DataOutputStream
including, as you will see in the networking chapter, a network. The output
from one run was:

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)

You
can see that the deserialized object really does contain all of the links that
were in the original object.

Note
that no constructor, not even the default constructor, is called in the process
of deserializing a
Serializable
object. The entire object is restored by recovering data from the
InputStream.

Finding
the class

You
might wonder what’s necessary for an object to be recovered from its
serialized state. For example, suppose you serialize an object and send it as a
file or through a network to another machine. Could a program on the other
machine reconstruct the object using only the contents of the file?

The
best way to answer this question is (as usual) by performing an experiment. The
following file goes in the subdirectory for this chapter:

//: Alien.java
// A serializable class
import java.io.*;
 
public class Alien implements Serializable {
} ///:~ 

The
file that creates and serializes an
Alien
object
goes in the same directory:

//: FreezeAlien.java
// Create a serialized output file
import java.io.*;
 
public class FreezeAlien {
  public static void main(String[] args)
      throws Exception {
    ObjectOutput out =
      new ObjectOutputStream(
        new FileOutputStream("file.x"));
    Alien zorcon = new Alien();
    out.writeObject(zorcon);
  }
} ///:~ 

Rather
than catching and handling exceptions, this program takes the quick and dirty
approach of passing the exceptions out of
main( ),
so they’ll be reported on the command line.

Once
the program is compiled and run, copy the resulting
file.x
to a subdirectory called
xfiles,
where the following code goes:

//: ThawAlien.java
// Try to recover a serialized file without the 
// class of object that's stored in that file.
package c10.xfiles;
import java.io.*;
 
public class ThawAlien {
  public static void main(String[] args)
      throws Exception {
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("file.x"));
    Object mystery = in.readObject();
    System.out.println(
      mystery.getClass().toString());
  }
} ///:~ 

This
program opens the file and reads in the object
mystery
successfully. However, as soon as you try to find out anything about the object
– which requires the
Class
object for
Alien
– the Java Virtual Machine (JVM) cannot find
Alien.class
(unless it happens to be in the Classpath, which it shouldn’t be in this
example). You’ll get a
ClassNotFoundException.
(Once again, all evidence of alien life vanishes before proof of its existence
can be verified!)

If
you expect to do much after you’ve recovered an object that has been
serialized, you must make sure that the JVM can find the associated
.class
file either in the local class path or somewhere on the Internet.

Controlling
serialization

As
you can see, the default serialization mechanism is trivial to use. But what if
you have special needs? Perhaps you have special security issues and you
don’t want to serialize portions of your object, or perhaps it just
doesn’t make sense for one sub-object to be serialized if that part needs
to be created anew when the object is recovered.

The
following example shows simple implementations of the
Externalizable
interface methods. Note that
Blip1
and
Blip2
are nearly identical except for a subtle difference (see if you can discover it
by looking at the code):

//: Blips.java
// Simple use of Externalizable & a pitfall
import java.io.*;
import java.util.*;
 
class Blip1 implements Externalizable {
  public Blip1() {
    System.out.println("Blip1 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip1.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip1.readExternal");
  }
}
 
class Blip2 implements Externalizable {
  Blip2() {
    System.out.println("Blip2 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip2.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip2.readExternal");
  }
}
 
public class Blips {
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blips.out"));
      System.out.println("Saving objects:");
      o.writeObject(b1);
      o.writeObject(b2);
      o.close();
      // Now get them back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blips.out"));
      System.out.println("Recovering b1:");
      b1 = (Blip1)in.readObject();
      // OOPS! Throws an exception:
//!   System.out.println("Recovering b2:");
//!   b2 = (Blip2)in.readObject();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

The
output for this program is:

Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal

The
reason that the
Blip2
object is not recovered is that trying to do so causes an exception. Can you
see the difference between
Blip1
and
Blip2?
The constructor for
Blip1
is
public,
while the constructor for
Blip2
is not, and that causes the exception upon recovery. Try making
Blip2’s
constructor
public
and
removing the
//!
comments
to see the correct results.

When
b1
is recovered, the
Blip1
default constructor is called. This is different from recovering a
Serializable
object, in which the object is constructed entirely from its stored bits, with
no constructor calls. With an
Externalizable
object, all the normal default construction behavior occurs (including the
initializations at the point of field definition), and
then
readExternal( )
is called. You need to be aware of this – in particular the fact that all
the default construction always takes place – to produce the correct
behavior in your
Externalizable
objects.

Here’s
an example that shows what you must do to fully store and retrieve an
Externalizable
object:

//: Blip3.java
// Reconstructing an externalizable object
import java.io.*;
import java.util.*;
 
class Blip3 implements Externalizable {
  int i;
  String s; // No initialization
  public Blip3() {
    System.out.println("Blip3 Constructor");
    // s, i not initialized
  }
  public Blip3(String x, int a) {
    System.out.println("Blip3(String x, int a)");
    s = x;
    i = a;
    // s & i initialized only in non-default
    // constructor.
  }
  public String toString() { return s + i; }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip3.writeExternal");
    // You must do this:
    out.writeObject(s); out.writeInt(i);
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip3.readExternal");
    // You must do this:
    s = (String)in.readObject();
    i =in.readInt();
  }
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    System.out.println(b3.toString());
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blip3.out"));
      System.out.println("Saving object:");
      o.writeObject(b3);
      o.close();
      // Now get it back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blip3.out"));
      System.out.println("Recovering b3:");
      b3 = (Blip3)in.readObject();
      System.out.println(b3.toString());
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

The
fields
s
and
i
are
initialized only in the second constructor, but not in the default constructor.
This means that if you don’t initialize
s
and
i
in
readExternal,
it will be
null
(since the storage for the object gets wiped to zero in the first step of
object creation). If you comment out the two lines of code following the
phrases “You must do this” and run the program, you’ll see
that when the object is recovered,
s
is
null
and
i
is zero.

If
you are inheriting from an
Externalizable
object, you’ll typically call the base-class versions of
writeExternal( )
and
readExternal( )
to provide proper storage and retrieval of the base-class components.

So
to make things work correctly you must not only write the important data from
the object during the
writeExternal( )
method
(there is no default behavior that writes any of the member objects for an
Externalizable
object), but you must also recover that data in the
readExternal( )
method. This can be a bit confusing at first because the default construction
behavior for an
Externalizable
object can make it seem like some kind of storage and retrieval takes place
automatically. It does not.


The
transient keyword

When
you’re controlling serialization, there might be a particular subobject
that you don’t want Java’s serialization mechanism to automatically
save and restore. This is commonly the case if that subobject represents
sensitive information that you don’t want to serialize, such as a
password. Even if that information is
private
in
the object, once it’s serialized it’s possible for someone to
access it by reading a file or intercepting a network transmission.

One
way to prevent sensitive parts of your object from being serialized is to
implement your class as
Externalizable,
as shown previously. Then nothing is automatically serialized and you can
explicitly serialize only the necessary parts inside
writeExternal( ).

//: Logon.java
// Demonstrates the "transient" keyword
import java.io.*;
import java.util.*;
 
class Logon implements Serializable {
  private Date date = new Date();
  private String username;
  private transient String password;
  Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
  public String toString() {
    String pwd =
      (password == null) ? "(n/a)" : password;
    return "logon info: n   " +
      "username: " + username +
      "n   date: " + date.toString() +
      "n   password: " + pwd;
  }
  public static void main(String[] args) {
    Logon a = new Logon("Hulk", "myLittlePony");
    System.out.println( "logon a = " + a);
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Logon.out"));
      o.writeObject(a);
      o.close();
      // Delay:
      int seconds = 5;
      long t = System.currentTimeMillis()
             + seconds * 1000;
      while(System.currentTimeMillis() < t)
        ;
      // Now get them back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Logon.out"));
      System.out.println(
        "Recovering object at " + new Date());
      a = (Logon)in.readObject();
      System.out.println( "logon a = " + a);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

You
can see that the
date
and
username
fields are ordinary (not
transient),
and thus are automatically serialized. However, the
password
is
transient,
and so is not stored to disk; also the serialization mechanism makes no attempt
to recover it. The output is:

logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: myLittlePony
Recovering object at Sun Mar 23 18:25:59 PST 1997
logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: (n/a)

When
the object is recovered, the
password
field is
null.
Note that
toString( )
must check for a
null
value of
password
because
if you try to assemble a
String
object using the overloaded ‘
+
operator, and that operator encounters a
null
handle, you’ll get a
NullPointerException.
(Newer versions of Java might contain code to avoid this problem.)

You
can also see that the
date
field is stored to and recovered from disk and not generated anew.

Since
Externalizable
objects do not store any of their fields by default, the
transient
keyword is for use with
Serializable
objects only.


An
alternative to Externalizable

The
methods must have these exact signatures:

private void
  writeObject(ObjectOutputStream stream)
    throws IOException;
 
private void
  readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException

From
a design standpoint, things get really weird here. First of all, you might
think that because these methods are not part of a base class or the
Serializable
interface, they ought to be defined in their own interface(s). But notice that
they are defined as
private,
which means they are to be called only by other members of this class. However,
you don’t actually call them from other members of this class, but
instead the
writeObject( )
and
readObject( )
methods of the
ObjectOutputStream
and
ObjectInputStream
objects call your object’s
writeObject( )
and
readObject( )
methods. (Notice my tremendous restraint in not launching into a long diatribe
about using the same method names here. In a word: confusing.) You might wonder
how the
ObjectOutputStream
and
ObjectInputStream
objects have access to
private
methods of your class. We can only assume that this is part of the
serialization magic.

In
any event, anything defined in an
interface
is automatically
public
so if
writeObject( )
and
readObject( )
must be
private,
then they can’t be part of an
interface.
Since you must follow the signatures exactly, the effect is the same as if
you’re implementing an
interface.

It
would appear that when you call
ObjectOutputStream.writeObject( ),
the
Serializable
object that you pass it to is interrogated (using reflection, no doubt) to see
if it implements its own
writeObject( ).
If so, the normal serialization process is skipped and the
writeObject( )
is called. The same sort of situation exists for
readObject( ).

There’s
one other twist. Inside your
writeObject( ),
you can choose to perform the default
writeObject( )
action by calling
defaultWriteObject( ).
Likewise, inside
readObject( )
you can call
defaultReadObject( ).
Here is a simple example that demonstrates how you can control the storage and
retrieval of a
Serializable
object:

//: SerialCtl.java
// Controlling serialization by adding your own
// writeObject() and readObject() methods.
import java.io.*;
 
public class SerialCtl implements Serializable {
  String a;
  transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() {
    return a + "n" + b;
  }
  private void
    writeObject(ObjectOutputStream stream)
      throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void
    readObject(ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args) {
    SerialCtl sc =
      new SerialCtl("Test1", "Test2");
    System.out.println("Before:n" + sc);
    ByteArrayOutputStream buf =
      new ByteArrayOutputStream();
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(buf);
      o.writeObject(sc);
      // Now get it back:
      ObjectInputStream in =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf.toByteArray()));
      SerialCtl sc2 = (SerialCtl)in.readObject();
      System.out.println("After:n" + sc2);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

The
storage and retrieval of the
transient
objects uses more familiar code. And yet, think about what happens here. In
main( ),
a
SerialCtl
object is created, and then it’s serialized to an
ObjectOutputStream.
(Notice in this case that a buffer is used instead of a file – it’s
all the same to the
ObjectOutputStream.)
The serialization occurs in the line:

o.writeObject(sc);

The
writeObject( )
method must be examining
sc
to see if it has its own
writeObject( )
method. (Not by checking the interface – there isn’t one – or
the class type, but by actually hunting for the method using reflection.) If it
does, it uses that. A similar approach holds true for
readObject( ).
Perhaps this was the only practical way that they could solve the problem, but
it’s certainly strange.


Versioning

Using
persistence

Here’s
an example that shows the problem:

//: MyWorld.java
import java.io.*;
import java.util.*;
 
class House implements Serializable {}
 
class Animal implements Serializable {
  String name;
  House preferredHouse;
  Animal(String nm, House h) {
    name = nm;
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() +
      "], " + preferredHouse + "n";
  }
}
 
public class MyWorld {
  public static void main(String[] args) {
    House house = new House();
    Vector  animals = new Vector();
    animals.addElement(
      new Animal("Bosco the dog", house));
    animals.addElement(
      new Animal("Ralph the hamster", house));
    animals.addElement(
      new Animal("Fronk the cat", house));
    System.out.println("animals: " + animals);
 
    try {
      ByteArrayOutputStream buf1 =
        new ByteArrayOutputStream();
      ObjectOutputStream o1 =
        new ObjectOutputStream(buf1);
      o1.writeObject(animals);
      o1.writeObject(animals); // Write a 2nd set
      // Write to a different stream:
      ByteArrayOutputStream buf2 =
        new ByteArrayOutputStream();
      ObjectOutputStream o2 =
        new ObjectOutputStream(buf2);
      o2.writeObject(animals);
      // Now get them back:
      ObjectInputStream in1 =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf1.toByteArray()));
      ObjectInputStream in2 =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf2.toByteArray()));
      Vector animals1 = (Vector)in1.readObject();
      Vector animals2 = (Vector)in1.readObject();
      Vector animals3 = (Vector)in2.readObject();
      System.out.println("animals1: " + animals1);
      System.out.println("animals2: " + animals2);
      System.out.println("animals3: " + animals3);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~ 

One
thing that’s interesting here is that it’s possible to use object
serialization to and from a byte array as a way of doing a “deep
copy” of any object that’s
Serializable.
(A deep copy means that you’re duplicating the entire web of objects,
rather than just the basic object and its handles.) Copying is covered in depth
in Chapter 12.

Animal
objects contain fields of type
House.
In
main( ),
a
Vector
of these
Animals
is created and it is serialized twice to one stream and then again to a
separate stream. When these are deserialized and printed, you see the following
results for one run (the objects will be in different memory locations each run):

animals: [Bosco the dog[Animal@1cc76c], House@1cc769
, Ralph the hamster[Animal@1cc76d], House@1cc769
, Fronk the cat[Animal@1cc76e], House@1cc769
]
animals1: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals2: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals3: [Bosco the dog[Animal@1cca52], House@1cca5c
, Ralph the hamster[Animal@1cca5d], House@1cca5c
, Fronk the cat[Animal@1cca61], House@1cca5c
]

Of
course you expect that the deserialized objects have different addresses from
their originals. But notice that in
animals1
and
animals2
the same addresses appear, including the references to the
House
object
that both share. On the other hand, when
animals3
is
recovered the system has no way of knowing that the objects in this other
stream are aliases of the objects in the first stream, so it makes a completely
different web of objects.

As
long as you’re serializing everything to a single stream, you’ll be
able to recover the same web of objects that you wrote, with no accidental
duplication of objects. Of course, you can change the state of your objects in
between the time you write the first and the last, but that’s your
responsibility – the objects will be written in whatever state they are
in (and with whatever connections they have to other objects) at the time you
serialize them.

The
safest thing to do if you want to save the state of a system is to serialize as
an “atomic” operation. If you serialize some things, do some other
work, and serialize some more, etc., then you will not be storing the system
safely. Instead, put all the objects that comprise the state of your system in
a single collection and simply write that collection out in one operation. Then
you can restore it with a single method call as well.

The
following example is an imaginary computer-aided design (CAD) system that
demonstrates the approach. In addition, it throws in the issue of
static
fields – if you look at the documentation you’ll see that
Class
is
Serializable,
so it should be easy to store the
static
fields by simply serializing the Class
object. That seems like a sensible approach, anyway.

//: CADState.java
// Saving and restoring the state of a 
// pretend CAD system.
import java.io.*;
import java.util.*;
 
abstract class Shape implements Serializable {
  public static final int
    RED = 1, BLUE = 2, GREEN = 3;
  private int xPos, yPos, dimension;
  private static Random r = new Random();
  private static int counter = 0;
  abstract public void setColor(int newColor);
  abstract public int getColor();
  public Shape(int xVal, int yVal, int dim) {
    xPos = xVal;
    yPos = yVal;
    dimension = dim;
  }
  public String toString() {
    return getClass().toString() +
      " color[" + getColor() +
      "] xPos[" + xPos +
      "] yPos[" + yPos +
      "] dim[" + dimension + "]n";
  }
  public static Shape randomFactory() {
    int xVal = r.nextInt() % 100;
    int yVal = r.nextInt() % 100;
    int dim = r.nextInt() % 100;
    switch(counter++ % 3) {
      default:
      case 0: return new Circle(xVal, yVal, dim);
      case 1: return new Square(xVal, yVal, dim);
      case 2: return new Line(xVal, yVal, dim);
    }
  }
}
 
class Circle extends Shape {
  private static int color = RED;
  public Circle(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}
 
class Square extends Shape {
  private static int color;
  public Square(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
    color = RED;
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}
 
class Line extends Shape {
  private static int color = RED;
  public static void
  serializeStaticState(ObjectOutputStream os)
      throws IOException {
    os.writeInt(color);
  }
  public static void
  deserializeStaticState(ObjectInputStream os)
      throws IOException {
    color = os.readInt();
  }
  public Line(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}
 
public class CADState {
  public static void main(String[] args)
      throws Exception {
    Vector shapeTypes, shapes;
    if(args.length == 0) {
      shapeTypes = new Vector();
      shapes = new Vector();
      // Add handles to the class objects:
      shapeTypes.addElement(Circle.class);
      shapeTypes.addElement(Square.class);
      shapeTypes.addElement(Line.class);
      // Make some shapes:
      for(int i = 0; i < 10; i++)
        shapes.addElement(Shape.randomFactory());
      // Set all the static colors to GREEN:
      for(int i = 0; i < 10; i++)
        ((Shape)shapes.elementAt(i))
          .setColor(Shape.GREEN);
      // Save the state vector:
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("CADState.out"));
      out.writeObject(shapeTypes);
      Line.serializeStaticState(out);
      out.writeObject(shapes);
    } else { // There's a command-line argument
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream(args[0]));
      // Read in the same order they were written:
      shapeTypes = (Vector)in.readObject();
      Line.deserializeStaticState(in);
      shapes = (Vector)in.readObject();
    }
    // Display the shapes:
    System.out.println(shapes);
  }
} ///:~ 

Circle
and
Square
are straightforward extensions of
Shape;
the only difference is that
Circle
initializes
color
at the point of definition and
Square
initializes it in the constructor. We’ll leave the discussion of
Line
for later.

In
main( ),
one
Vector
is used to hold the
Class
objects and the other to hold the shapes. If you don’t provide a command
line argument the
shapeTypes
Vector
is created and the
Class
objects are added, and then the
shapes
Vector
is created and
Shape
objects are added. Next, all the
static
color
values are set to
GREEN,
and everything is serialized to the file
CADState.out.

If
you provide a command line argument (presumably
CADState.out),
that file is opened and used to restore the state of the program. In both
situations, the resulting
Vector
of
Shapes
is printed out. The results from one run are:

>java CADState
[class Circle color[3] xPos[-51] yPos[-99] dim[38]
, class Square color[3] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[3] xPos[-70] yPos[1] dim[16]
, class Square color[3] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[3] xPos[-75] yPos[-43] dim[22]
, class Square color[3] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[3] xPos[17] yPos[90] dim[-76]
]
 
>java CADState CADState.out
[class Circle color[1] xPos[-51] yPos[-99] dim[38]
, class Square color[0] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[1] xPos[-70] yPos[1] dim[16]
, class Square color[0] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[1] xPos[-75] yPos[-43] dim[22]
, class Square color[0] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[1] xPos[17] yPos[90] dim[-76]
]

You
can see that the values of
xPos,
yPos,
and
dim
were all stored and recovered successfully, but there’s something wrong
with the retrieval of the
static
information. It’s all ‘3’ going in, but it doesn’t come
out that way.
Circles
have a value of 1 (
RED,
which is the definition), and
Squares
have a value of 0 (remember, they are initialized in the constructor).
It’s as if the
statics
didn’t get serialized at all! That’s right – even though class
Class
is
Serializable,
it doesn’t do what you expect. So if you want to serialize
statics,
you must do it yourself.

This
is what the
serializeStaticState( )
and
deserializeStaticState( )
static
methods
in
Line
are for. You can see that they are explicitly called as part of the storage and
retrieval process. (Note that the order of writing to the serialize file and
reading back from it must be maintained.) Thus to make
CADState.java
run correctly you must (1) Add a
serializeStaticState( )
and
deserializeStaticState( )
to the shapes, (2) Remove the
Vector
shapeTypes

and all code related to it, and (3) Add calls to the new serialize and
deserialize static methods in the shapes.

Another
issue you might have to think about is security, since serialization also saves
private
data. If you have a security issue, those fields should be marked as
transient.
But then you have to design a secure way to store that information so that when
you do a restore you can reset those
private
variables.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read