Main Page

Previous Next

Implementing the Serializable Interface

As I hope you still remember, the fundamental step in making objects serializable is to implement the Serializable interface in every class that defines objects we want written to a file. We need a methodical approach here, so how about top-down – starting with the SketchModel class.

Try It Out – Serializing SketchModel Objects

This is where we get a great deal from astonishingly little effort. To implement serialization for the SketchModel class you must first modify the class definition header to:

class SketchModel extends Observable implements Serializable {

The Serializable interface is defined in the package java.io so we need to add an import statement to the beginning of the SketchModel.java file:

import java.io.Serializable;

The Serializable interface declares no methods – so that's it!

Is that enough to serialize a sketch? Not quite. For a class to be serializable, all its data members must be serializable or declared as transient. If this is not the case then an exception of type NotSerializableException will be thrown. To avoid this we must trawl through the data elements of the SketchModel class, and if any of these are our own classes we must make sure they either implement the Serializable interface, or are declared as transient.

We also must not assume that objects of a standard class type are serializable, because some most definitely are not. It's a fairly quick fishing trip though, because our SketchModel class only has one data member – the linked list of elements that make up the sketch. If the SketchModel object is to be serializable we simply need to make sure the elementList member is serializable.

Serializing the List of Elements

Looking through the javadocs we can see that the LinkedList class is serializable, so all we need to worry about are the list elements themselves. We can make the base class for our shape class, Element, serializable by declaring that it implements the interface:

public abstract class Element implements Serializable {

Don't forget that we now need an import statement for the Serializable interface in Element.java. Since we will be using several classes from the java.io package, let's save a few lines and import all the names into Element.java:

import java.io.*;

The data members of the Element class that are object references are of type Color or of type Point, and since both of these classes are serializable, as you can verify from the SDK documentation, our Element class is serializable. Now we need to look at the subclasses of Element.

Subclasses of Element will inherit the implementation of the Serializable interface but there is a tiny snag. At the time of writing, none of the Shape classes in the java.awt.geom package are serializable, and we have been using them all over the place.

We are not completely scuppered though. Remember that you can always implement the readObject() and writeObject() methods in a class and then implement your own serialization. We can take the data that we need to recreate the required Shape object and serialize that in our implementation of the writeObject() method. We will then be able to reconstruct the object from the data in the readObject() method. Let's start with our Element.Line class.

Serializing Lines

Just to remind you of one of the things we discussed back in the I/O chapters, the writeObject() method that serializes objects must have the form:

private void writeObject(ObjectOutputStream out) throws IOException {
  // Code to serialize the object...
}

Our Element.Line objects are always drawn from (0, 0) so there's no sense in saving the start point in a line – it's always the same. We just need to serialize the end point, so add the following to the Element.Line class:

private void writeObject(ObjectOutputStream out) throws IOException {
  out.writeDouble(line.x2);
  out.writeDouble(line.y2);
}

We don't need to worry about exceptions that might be thrown by the writeDouble() method at this point. These will be passed on to the method that calls writeObject(). The coordinates are public members of the Line2D.Double object so we can reference them directly to write them to the stream. The rest of the data relating to a line is stored in the base class, Element, and as we said earlier they are all taken care of. The base class members will be serialized automatically when an Element.Line object is written to a file. We just need the means to read it back.

To recap what you already know, the readObject() method to deserialize an object is also of a standard form:

private void readObject(ObjectInputStream in)
             throws IOException,  ClassNotFoundException {
  // Code to deserialize an object...
}

For the Line class, the implementation will read the coordinates of the end point of the line and reconstitute line – the Line2D.Double member of the class. Adding the following method to the Element.Line class will do that:

private void readObject(java.io.ObjectInputStream in)
             throws IOException, ClassNotFoundException {
  double x2 = in.readDouble();
  double y2 = in.readDouble();
  line = new Line2D.Double(0,0,x2,y2);
}

That's lines serialized. Looks as though it's going to be easy. We can do rectangles next.

Serializing Rectangles

A rectangle is always drawn with its top left corner at the origin, so we only need to write the width and height to the file:

private void writeObject(ObjectOutputStream out) throws IOException {
  out.writeDouble(rectangle.width);
  out.writeDouble(rectangle.height);
}

The width and height members of the Rectangle2D.Double object are public, so we can access them directly to write them to the stream.

Deserializing an Element.Rectangle object is almost identical to the way we desirialized a line:

private void readObject(ObjectInputStream in)
             throws IOException, ClassNotFoundException {
  double width = in.readDouble();
  double height = in.readDouble();
  rectangle = new Rectangle2D.Double(0,0,width,height);
}

An Element.Circle object is actually going to be easier.

Serializing Circles

A circle is drawn as an ellipse with the top left corner of the bounding rectangle at the origin. The only item of data we will need to reconstruct a circle is the diameter:

private void writeObject(ObjectOutputStream out) throws IOException {
  out.writeDouble(circle.width);
}

The diameter is recorded in the width member (and also in the height member) of the Ellipse2D.Double object. We just write it to the file.

We can read a circle back with the following code:

private void readObject(ObjectInputStream in)
             throws IOException, ClassNotFoundException {
  double width = in.readDouble();
  circle = new Ellipse2D.Double(0,0,width,width);
}

This reconstitutes the circle using the diameter that was written to the file.

Serializing Curves

Curves are a little trickier. One complication is that we create a curve as a GeneralPath object, and we have no idea how many segments make up the curve. We can obtain a special Iterator object of type PathIterator for a GeneralPath object that will make available to us all the information necessary to create the GeneralPath object. PathIterator is an interface that declares methods for retrieving details of the segments that make up a GeneralPath object, so a reference to an object of type PathIterator encapsulates all the data defining that path.

The getPathIterator() method in the GeneralPath class returns a reference of type PathIterator. The argument to getPathIterator() is an AffineTransform object that is applied to the path. This is based on the assumption that a single GeneralPath object may be used to create a number of different appearances on the screen.

You might have a GeneralPath object that defines a complicated object, a boat for example. You could draw several boats on the screen simply by applying a transform before you draw each boat to set its position and orientation and use the same GeneralPath object for all. This avoids the overhead of creating multiple instances of what are essentially identical objects. That's why the getIterator() method enables you to obtain an iterator for a particular transformed instance of a GeneralPath object. However, we want an iterator for the unmodified path to get the basic data that we need, so we pass a default AffineTranform object, which does nothing.

The PathIterator interface declares four methods:

Method

Description

currentSegment(
double[] coords)

currentSegment(
float[] coords)

Returns the current segment. See the text following this table for a detailed description.

getWindingRule()

Returns a value of type int defining the winding rule. The value can be WIND_EVEN_ODD or WIND_NON_ZERO.

next()

Moves the iterator to the next segment as long as there is another segment.

isDone()

Returns true if the iteration is complete, and false otherwise.

The array argument, coords, that you pass to either version of the currentSegment() method is used to store data relating to the current segment, and should have six elements to record the coordinates of one, two, or three points, depending on the current segment type.

The method returns an int value that indicates the type of the segment, and can be one of the following values:

Segment Type

Description

SEG_MOVETO

The segment corresponds to a moveTo() operation. The coordinates of the point moved to are returned as the first two elements of the array, coords.

SEG_LINETO

The segment corresponds to a lineTo() operation. The coordinates of the end point of the line are returned as the first two elements of the array, coords.

SEG_QUADTO

The segment corresponds to a quadTo() operation. The coordinates of the control point for the quadratic segment are returned as the first two elements of the array, coords, and the end point is returned as the third and fourth elements.

SEG_CUBICTO

The segment corresponds to a curveTo() operation. The array coords will contain coordinates of the first control point, the second control point, and the end point of the cubic curve segment.

SEG_CLOSE

The segment corresponds to a closePath() operation. The segment closes the path by connecting the current point to the first point in the path. No values are returned in the coords array.

We have all the tools we need to get the data on every segment in the path. We just need to get a PathIterator reference and use the next() method to go through it. Our case is simple: we only have a single moveTo() segment – always to (0, 0) – followed by one or more lineTo() segments. We will still test the return type, though, to show how it's done, and in case there are errors. We're going to end up with an array of coordinates with an unpredictable number of elements: it sounds like a case for a Vector, particularly since Vector objects are serializable.

A Vector object only stores object references, not the float values that we have, so we'll have to convert our coordinate values to objects of type Float before storing them. The first segment is a special case. It is always a move to (0, 0), whereas all the others will be lines. Thus the procedure will be to get the first segment and discard it after verifying it is a move, and then get the remaining segments in a loop. Here's the code:

private void writeObject(ObjectOutputStream out) throws IOException {
  PathIterator iterator = curve.getPathIterator(new AffineTransform());
  Vector coords = new Vector();
  int maxCoordCount = 6;
  float[] temp = new float[maxCoordCount];  // Stores segment data

  int result = iterator.currentSegment(temp);  // Get first segment
  assert(result == iterator.SEG_MOVETO);
  iterator.next();                         // Next segment
  while(!iterator.isDone()) {              // While we have segments
    result = iterator.currentSegment(temp);      // Get the segment data
    assert(result == iterator.SEG_LINETO);

    coords.add(new Float(temp[0]));        // Add x coordinate to Vector
    coords.add(new Float(temp[1]));          // Add y coordinate
    iterator.next();                         // Go to next segment
  }

  out.writeObject(coords);                      // Save the Vector
}

We obtain a java.awt.geom.PathIterator object for the Element.Curve object that we will use to extract the segment data for the curve. We will have to import this class into Element.java. We create a Vector object in which we will store the coordinate data as objects and we will serialize this vector in the serialization of the curve. We also create a float[] array to hold the numerical coordinate values for a segment. All six elements are used when the segment is a cubic Bezier curve. In our case fewer are used but we must still supply an array with six elements as an argument to the currentSegment() method because that's what the method expects to receive.

After verifying that the first segment is a move-to segment, we use the path iterator to extract the segment data that defines the curve. We use the Float class constructor to create Float objects to store in the Vector object coords. The assertion is there to make sure that the curve consists only of line segments after the initial move-to.

We will need an import statement for Vector to make the class accessible in the Element.java source file:

import java.util.Vector;

It's worth considering how we might handle a GeneralPath object that consisted of a variety of different segments in arbitrary sequence. For the case where the path consisted of a set of line, quad, or cubic segments, you could get away with using a Vector object to store the coordinates for each segment, and then store these objects in another Vector. You could deduce the type of segment from the number of coordinates you have stored in each Vector object for a segment since line, cubic, and quad segments each require a difference number of points. In the general case you would need to define classes to represent the segments of various types, plus moves, of course. If these had a common base class, then you could store all the objects for a path in a Vector as base class references. Of course, you would need to make sure your segment classes were serializable too.

To deserialize a curve, we just have to read the Vector object from the file, and recreate the GeneralPath object for the Element.Curve class:

private void readObject(ObjectInputStream in)
             throws IOException, ClassNotFoundException {
  Vector coords = (Vector)in.readObject();    // Read the coordinates Vector
  curve = new GeneralPath();                  // Create a path
  curve.moveTo(0,0);                          // Move to the origin
  float x, y;                                 // Stores coordinates

  for(int i = 0 ; i<coords.size() ; i += 2 ) { // For each pair of elements
    x = ((Float)coords.get(i)).floatValue();   // Get x value
    y = ((Float)coords.get(i+1)).floatValue(); // Get y value
    curve.lineTo(x,y);                         // Create a line segment
  }
}

This should be very easy to follow now. We read the data we wrote to the stream – the Vector of Float objects. The first segment is always a move to the origin. All the succeeding segments are lines specified by pairs of elements from the Vector. The floatValue() method for the Float objects that are stored in the Vector object returns the numerical coordinate values. We use these to create the line segments.

Serializing Text

Element.Text is the last element type we have to deal with. Fortunately, Font, String, and java.awt.Rectangle objects are all serializable already, which means that Element.Text is serializable by default and we have nothing further to do. We can now start implementing the listener operations for the File menu.

Previous Next
JavaScript Editor Java Tutorials Free JavaScript Editor