ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Learning Polymorphism and Object Serialization

by Budi Kurniawan
03/08/2001

While most textbooks teach polymorphism by drawing UML diagrams, the Brainy Draw project encourages you to draw. While others make up hypothetical objects to use as examples for object serialization, we'll discuss the real need to persist your objects. The Brainy Draw project in this article is a fun way to learn polymorphism and object serialization.

Polymorphism and object serialization are two important topics in Java. While object serialization is easy to grasp once you understand the concept of objects, polymorphism is sometimes a thorny subject. This article first introduces you with a brief theory of both topics, but won't spend too much time with theories, because there are heaps of decent Java books that explain those topics in details. This article, on the other hand, spends most of the space discussing the application in which polymorphism and object serialization is used. You can download the code and have a play with it. In this Internet and PS2 era, I still believe in the old saying "practice makes perfect."

Polymorphism

Polymorphism is one of the three fundamental principles in object oriented programming. The other two are encapsulation and inheritance. Of the three, I would say polymorphism is the hardest to comprehend. To learn polymorphism, you should understand inheritance.

Inheritance is a mechanism that allows a class to be defined as a specialization of another class. It is said that the more specialized class is a subclass or a child class of the more general class. The class where the child class is derived from is also called the parent class. Inheritance gets its name because the subclass inherits all the properties and methods from the parent class, except those that are private to the parent class. Why do we extend a class? For code reuse; the structure and behavior already implemented by one class may be used to define other classes. If someone else has already written a class that satisfies 85% of your need, you can just extend the class and save time by implementing only the remaining 15% of functionality.

Polymorphism is briefly described as "one interface, many implementations." When dealing with class hierarchies, you often want to treat an object not as the specific type that it is, but instead as its base type. This allows you to write code that doesn't depend on specific types.

This is often explained with the classic example, Shape. Consider the class hierarchy in Figure 1.

Figure 1.
Figure 1. The Shape base class is implemented by Rectangle, Oval and Line

In this hierarchy, Rectangle, Oval, and Line are more specialized forms of Shape. They share some basic behavior, though. If Shape has a method called draw, Rectangle, Oval, and Line inherit this method. However, the draw method inherited can be overridden in the child class. This method overriding is actually what makes things more interesting.

Because Rectangle, Oval and Line are derived from Shape, they are themselves Shapes. Therefore you can write something like the following code, for which the technical term is upcasting.

Shape shape = new Rectangle();

or

Shape shape = new Oval();

or

Shape shape = new Line();

It is legal to store a Rectangle object in a Shape object reference because a Rectangle is also a Shape. The other way around isn't true, though. A Shape is not always a Rectangle.

Now sometimes you have a Shape object reference containing an object of a subclass of Shape, but you don't know what exactly the object is, i.e. whether the object is a rectangle, oval, or line. However, you can call the draw method of Shape, and the correct implementation will be called. In other words, if the object happens to be Oval, the draw method in the Oval class is called. If it is a Line, then the draw method in Line is invoked.

Why this is very useful is explained better when you build the implementation in the Brainy Draw project.

Object serialization

With object serialization, you can take any Java object that implements the Serializable interface and turn it into a sequence of bytes stored in a file or sent across a network. These bytes can later be restored to regenerate the original object. This feature provides a lightweight persistence mechanism for your objects.

Object serialization also works across operating systems. This means you can run a Java application on Windows and serialize an object into a file. Later you can open this file on Linux and still obtain the same object. If the object you serialized held references to other objects, those other objects are serialized too, provided they also implement the Serializable interface.

The good thing about object serialization is Java takes care of the byte formats and ordering and all other details. This really saves a lot of coding time.

To serialize an object, you need some sort of OutputStream object that is wrapped inside an ObjectOutputStream object. Afterwards, you can use the writeObject method of the ObjectOutputStream class to serialize your object by passing the object as a parameter to the writeObject method.

To restore the object, you can reverse the process by wrapping an InputStream object inside an ObjectInputStream object. With this ObjectInputStream object, you can call its readObject method to obtain your object. What is returned by the readObject method is an Object object. You need to downcast it to the correct type to get the original object.

As mentioned above, this article does not intend to elaborate on theories. For more information, you are referred to Sun's Java site or many Java books in the bookstore. You can, however, see object serialization in action in the Brainy Draw project.

The Brainy Draw Project

The project that will highlight polymorphism and serialization in this article is called Brainy Draw. This is a vector-based graphic tool that allows you to draw different shapes of any size. An example of another vector-based graphics application is Corel Draw. In this type of drawing tool, each shape is an object that you paste on the drawing area. Each object is an independent object whose properties (thickness, color) can be changed and can be resized at any time during and after the drawing process. The other type of graphics application is a bitmap-based tool such as the Paint program in Windows. In this kind of application, each object is converted into a bitmap representation. Once you draw an object, the object blends with the other objects already on the drawing board. If you save the drawing and open it later, you won't be able to separate individual objects from each other because they are now all bitmaps.

The sole purpose of Brainy Draw's existence is to demonstrate polymorphism and object serialization in action. As such, Brainy Draw is kept simple, as a learning tool should be. Nevertheless, it satisfies the requirements of being called a graphics tool: Open New, Open, Save, and Undo features, as well as a click and drag feature to easily draw rectangles, ovals, and lines.

The three shapes demonstrate the use of polymorphism, and the Save and Open features shows object serialization at work.

Figure 3 shows the project graphical interface and Listing 1 gives the complete project code.

Figure 3
Figure 3. The Brainy Draw project.

Polymorphism: The Shape interface and three implementations

All the drawing shapes in Brainy Draw must implement the Shape interface. The Shape interface has only one method, the draw method. The signature of the draw method is given in the interface listed below.

interface Shape extends Serializable {
  public void draw(java.awt.Graphics g);
}

The draw method accepts one argument: an object reference of type java.awt.Graphics. It is intended that each class which implements Shape draws itself on this Graphics object. More information on how to obtain and use a Graphics object can be found in the "The Drawing Board" section.

There are three classes that implement the Shape interface: the Rectangle class, the Oval class, and the Lineclass. You can create you own classes if you feel like playing with Brainy Draw. As a rule, all classes must override draw. The classes are explained below.

The Rectangle Class

This class represents a rectangle with two coordinates: (x1, y1), which is the top-left corner of the rectangle; and (x2, y2), which is the bottom-right corner. The class signature and body are given below.

class Rectangle implements Shape {

  int x1, y1, x2, y2;

  public Rectangle(int x1, int y1, int x2, int y2) {
    this.x1 = x1 < x2? x1 : x2;
    this.y1 = y1 < y2? y1 : y2;
    this.x2 = x1 < x2? x2 : x1;
    this.y2 = y1 < y2? y2 : y1;
  }

  public void draw(Graphics g) {
    g.setColor(Color.green);
    g.fill3DRect(x1, y1, x2 - x1, y2 - y1, true);
    //g.drawRect(x1, y1, x2 - x1, y2 - y1);
  }
} // end of class Rectangle

The constructor accepts four arguments, which are the coordinate where the user clicks the mouse (x1, y1) and the coordinate where the user releases the mouse (x2, y2). Because it is not possible to force the user to draw a rectangle by always dragging the mouse rightwards and downwards, x1 and x2 must be swapped if the mouse happens to move to the left. By the same token, y1 and y2 must be swapped if the mouse moves up.

The draw method uses the fill3DRect method of the Graphics class to draw a rectangle. An alternative, which is commented out in the code, is to use the drawRect method. The signature of the drawRect method is as follows.

public void drawRect(int x, int y, int width, int height)

The signature of the fill3DRect method is as follows.

public void fill3DRect(int x, int y, int width, int height, boolean raised)

Both methods accepts the following arguments:

Knowing both the top-left corner and right-bottom corner coordinates allows us to calculate the width and the height of the rectangle.

Also, to make it more colorful, a green color is used to draw a rectangle, distinguishing it from other types of shapes that use different colors.

The Oval class

The Oval class represents an oval. The class signature and body are given below.

class Oval implements Shape {

  int x1, y1, x2, y2;

  public Oval(int x1, int y1, int x2, int y2) {
    this.x1 = x1 < x2? x1 : x2;
    this.y1 = y1 < y2? y1 : y2;
    this.x2 = x1 < x2? x2 : x1;
    this.y2 = y1 < y2? y2 : y1;
  }

  public void draw(Graphics g) {
    g.setColor(Color.yellow);
    g.fillOval(x1, y1, x2 - x1, y2 - y1);
    //g.drawOval(x1, y1, x2 - x1, y2 - y1);
  }
} // end of class Oval

Like that of the Rectangle class, the Oval class constructor accepts four arguments, which are the coordinate where the user clicks the mouse, (x1, y1), and the coordinate where the user releases the mouse, (x2, y2). Therefore, the same adjustments must occur if the user drags the mouse upwards or leftwards.

The draw method uses the fillOval method of the Graphics class to draw an oval. An alternative, which is commented out in the code, is to use the drawOval method. The signature of the fillOval method is as follows.

public abstract void fillOval(int x, int y, int width, int height)

The signature of the drawOval method is as follows.

public abstract void fillOval(int x, int y, int width, int height)

The arguments for both methods are as follow.

In Brainy Draw, an oval is always yellow.

The Line class

The Line class represents a straight line. The line can be drawn if the coordinates of two end points are known. The signature and body of this class are as follow.

class Line implements Shape {

  int x1, y1, x2, y2;

  public Line(int x1, int y1, int x2, int y2) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
  }

  public void draw(Graphics g) {
    g.setColor(Color.red);
    g.drawLine(x1, y1, x2, y2);
  }
} // end of class Line

Unlike the Rectangle and Oval classes, it is legal to draw any line with any angle within the coordinate system. Adjustments in the constructor are therefore not necessary. The draw method simply calls the drawLine method of the Graphics class. The drawLine method has the following signature.

public abstract void drawLine(int x1, int y1, int x2, int y2)

The method draws a straight line between the points (x1, y1) and (x2, y2).

This method has the following parameters.

A line is drawn with the red color.

The drawing board

When drawing graphics on a Java application, one thing is always of concern. How do you draw on the Frame? In an applet, this is easy because Applet's paint method is passed a very useful parameter: an object reference of type java.awt.Graphics. Once you have the Graphics object, drawing is easy. The Graphics class has nice methods to draw many things in a color of your choice. These methods include drawArc to draw the outline of a circular or elliptical arc, drawLine to draw a line, drawPolyline to draw a sequence of connected lines, drawRect to draw a rectangle, and so on. However, with an application you have a Frame, and you can't add a Graphics object because it is not a Component. You would like to somehow initiate the abstract Graphics class and turn it into a Component. You might think of using a Panel. However, you have the same problem with a Panel as with a Frame.

There is the Canvas class, which is meant to be extended. What's more, according to Sun's documentation, "A Canvas component represents a blank rectangular area of the screen onto which the application can draw or from which the application can trap input events from the user. An application must subclass the Canvas class in order to get useful functionality such as creating a custom component. The paint method must be overridden in order to perform custom graphics on the canvas."

Sounds perfect. A Canvas even has the paint method that can perform custom graphics on its body. But, it said that a Canvas is a blank rectangular area. This has profound implication if it is to be used to draw shapes in our Brainy Draw. Everything would take a rectangular space, including an oval and a line. When you draw an oval on top of a rectangle, the corners of the oval will occupy some space. Also, if you draw a line, the space taken is not only for the dots that compose the line, but a rectangular area. And a Canvas is not hollow, so it always covers a rectangular space even though you are drawing an unfilled rectangle.

Figure 2 shows the unexpected side effects. The oval and the rectangle don't physically touch each other. However, the top-right corner of the rectangle is invisible because the rectangular area of the oval occupies it.

Figure 2.
Figure 2. The Canvas component is not a good solution for Brainy Draw.

I decided to use what had been considered the ideal candidate for the drawing board in the first place: an applet. Though it doesn't sound conventional, using an applet with a Java application is not forbidden and does not necessarily turn your application into a Web browser. As you can see later, it works as expected. The coding part is also not hard. You just need a class that extends java.applet.Applet to utilize its paint method. Once you have a Graphics object that comes free with the paint method, you can use its methods to do a lot of drawing. Now, instead of a drawing panel that is an object of Panel, you have a panel that's a subclass of Applet. Adding the applet to the Frame is also very simple using the add(Component c) method because Applet is derived from Component.

The signature and body of the PanelApplet class, the new class that you use as a drawing board, is given below.

import java.applet.Applet;
import java.util.*;

class PanelApplet extends Applet {

  Vector shapes = new Vector();

  public void paint(Graphics g) {
    Enumeration e = shapes.elements();
    while (e.hasMoreElements()) {
      Shape s = (Shape) e.nextElement();
      s.draw(g);
    }
  }
} // end of PanelApplet class

Vector shapes is a container for all the objects drawn (rectangles, ovals, and lines). An element will be added when the user draws a shape and removed when the user clicks Undo. To display these objects correctly on the screen, you need to tell the paint method to loop through the Vector and draws the elements in the correct order.

The most interesting part of the overridden paint method are the following lines.

Shape s = (Shape) e.nextElement();
s.draw(g);

Each element (which is a Rectangle object, an Oval object, or a Line object) is upcast to a Shape object; and without having to know whether the Shape object is actually a Rectangle, an Oval, or a Line object, you can call the draw method. All because, and this is the magic of polymorphism, the JVM knows which draw method to call. If the Shape object is a Rectangle, the draw method in the Rectangle class is called. If it is a Line object, Java will invoke the draw method in the Line class. As a result, each object will be correctly drawn by the paint method.

The BrainyDraw Class

The BrainyDraw class is the main class that integrates all the classes explained so far. The following are some details on how each part works.

The Constructor

The constructor starts by calling the Frame parent class constructor to create a Frame object.

super("Brainy Draw");

It then instantiates a menu bar with two menus (File and Edit). The File menu has the following menu items: New, Open, Save, and Exit. The Edit menu has one menu item: Undo. After adding ActionListener objects to all the menu items, it creates a CheckboxGroup containing three check boxes for the three types of shapes. The user clicks an appropriate check box to draw a shape. ItemListener objects are added to these check boxes.

It then continues by adding the MouseListener object to panel (PanelApplet object reference). It then adds a WindowListener to itself, so the user can clicks the X button to close the window.

Drawing a Shape

To draw a shape, the user clicks on one of the three check boxes to select the type of the shape. Then the user clicks on the drawing panel and drags the mouse to start drawing. The position where the mouse is pressed becomes the first coordinate of the shape and the position where the mouse is released becomes the second coordinate. Two MouseListener methods are overridden: the mousePressed(MouseEvent me) method and the mouseReleased(MouseEvent me) method.

In the mousePressed method, you use the getX and getY methods of the MouseEvent object to get the first coordinate, which is passed to the class variables x1 and y1.

  public void mousePressed(MouseEvent me) {
    x1 = me.getX();
    y1 = me.getY();
  }

The mouseReleased method does the rest. It first takes the position where the mouse is released and assigns it as the second coordinate to the class variables x2 and y2. The rest of mouseReleased adds the shape to the Vector shapes. It knows the shape type by checking the shapeType value. For Rectangle and Oval objects, valid objects can't have two coordinates with the same abscissas (zero height) or the same ordinates (zero width). For a Line object, it is a bit relaxed. A Line will be created unless the two coordinates are the same.

  public void mouseReleased(MouseEvent me) {
    x2 = me.getX();
    y2 = me.getY();
    Shape s = null;
    if (shapeType.equals("Rectangle")) {
      // a Rectangle cannot have a zero width or height
      if (x1!=x2 || y1!=y2)
        s = new Rectangle(x1, y1, x2, y2);
    }
    else if (shapeType.equals("Line")) {
      // a Rectangle cannot have a zero width or height
      if (x1!=x2 && y1!=y2)
        s = new Line(x1, y1, x2, y2);
    }
    else if (shapeType.equals("Oval")) {
      // an Oval cannot have a zero width or height
      if (x1!=x2 || y1!=y2)
        s = new Oval(x1, y1, x2, y2);
    }
    if (s!=null) {
      panel.shapes.add(s);
      panel.repaint();
    }
  }

Upon creating a Shape, it adds it to the shapes Vector in the panel applet. It then calls the repaint method of the applet to refresh the drawing area.

The Menu functions

Now that you know how to draw a shape, it is time to see what you can do with the menu items. The actionPerformed method from the ActionListener interface that BrainyDraw class implements redirects clicks on each menu item to the processing methods. The code for the actionPerformed method is given below. Note how the getActionCommand method of the ActionEvent class returns the source object that receives the action.

  public void actionPerformed(ActionEvent ae) {
    String command = ae.getActionCommand().toString();

    if (command.equals("Exit"))
      System.exit(0);
    else if (command.equals("New"))
      openNew();
    else if (command.equals("Open"))
      open();
    else if (command.equals("Save"))
      save();
    else if (command.equals("Undo"))
      undo();
  }

Undo

The Undo menu item is connected to the undo private method. The class signature and body are as follow.

  private void undo() {
    int shapeCount = panel.shapes.size();
    if (shapeCount!=0) {
      panel.shapes.removeElementAt(shapeCount-1);
      panel.repaint();
    }
  }

The undo method simply cancels the last object drawn. This is achieved by removing the last element in the shapes Vector in the applet. To update the display, you need to call the applet's repaint method.

Open New

Open New is connected to the openNew private method. It basically starts with a new document and clears all the objects drawn, if any. The openNew method is given in the following snippet.

private void openNew() {
    panel.shapes.removeAllElements();
    panel.repaint();
  }

Because all shape objects are stored in shapes Vector, clearing them is as simple as calling the removeAllElements method of the Vector object. As usual, the applet's repaint method is called to refresh the drawing area.

Save

The Save menu item is connected to the save private method. As the name indicates, this method saves all objects to a file. It does so by serializing an object using the writeObject method of the ObjectOutputStream class. The writeObject method is a really effective method because it not only serializes the object passed to it but also all other objects referenced by that object. To be serializable, the object passed must implement the Serializable interface. If the object passed references other objects, the other objects must also implement Serializable.

The signature and body of the save method are as follows.

  private void save() {
    try {
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream(filename));
      out.writeObject(panel.shapes);
      out.close(); // Also flushes output
    } 
    catch(Exception e) {
      e.printStackTrace();
    }
  }

Because all of the shape objects are stored in shapes Vector, you need only serialize the shapes Vector itself. All of the objects in shapes Vector will be serialized automatically. The Vector class implements a couple of interfaces, including Serializable, so you can pass shapes Vector to the writeObject method. The Rectangle, Oval, and Line objects in shapes implement the Shape interface, which itself extends the Serializable interface. Therefore, all the Rectangle, Oval, and Line objects in the shapes Vector are also serializable.

The objects are serialized to the file indicated by filename.

Open

The Open menu item is connected to the open private method. It does the opposite of what the save method does. It retrieves the objects previously saved into the file. It does so by using the readObject method of the ObjectInputStream class. The readObject returns an Object object, so in the open method it must be cast to Vector.

The signature and the body of the open method are given below.

  private void open() {
    try {
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream(filename));
      panel.shapes = (Vector) in.readObject();
      panel.repaint();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

Exit

The Exit menu item makes the application exit by calling the following simple line of code.

System.exit(0);

Conclusion

You have just seen how BrainyDraw demonstrates polymorphism and object serialization in practice. Polymorphism is shown by the draw method in the Shape interface which is overridden in the Rectangle, Oval, and Line classes. When the draw method in a Shape object is called, Java knows which draw method to call. As a result, the object can draw itself correctly.

BrainyDraw also demonstrates how easy it is to do object serialization in Java. In the save method, you saw how to write objects to a file. In the open method, you saw how to read the objects back. All of these are done with one single method call, the writeObject of the ObjectOutputStream and the readObject of the ObjectInputStream. The writeObject method not only serializes the object passed to it, but all objects referenced by the passed object. To be serializable, the object passed and all other objects referenced by it must implement the Serializable interface.

Budi Kurniawan is a senior J2EE architect and author.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.