CS 5 Spring 2008
Lecture 19
April 28

Abstract Classes and a Hierarchy of Graphical Objects

In a previous lecture, we used an interface to allow ourselves to define a variable declared to be a reference to a GeomShape. In fact, GeomShape was an interface, and the way we implemented the program, the variable was actually a reference to either a Circle or a Rect object. We can accomplish the same thing via inheritance and avoid duplicating code in the process.

Consider geometric shapes: circles, rectangles, segments, squares, diamonds, etc. What do they have in common? We might want all of them to have a color, to know how to draw themselves, to know how to move themselves, and to know how to compute their area. The class GeomShape.java defines a class with these properties. Note that all shapes will have a color, and so myColor is an instance variable that is initialized in a constructor. There are methods to set and get the color.

The methods areaOf and move depend on specific information about the shape. Therefore they are given as abstract methods. The header ends in a semicolon and the word abstract is included in the header. The entire class is also declared as abstract. No object of an abstract class can ever be created. Hence, no GeomShape object can be created. Abstract classes exist only to serve as superclasses for other classes.

The draw method doesn't know how to draw the object. However, if it wants to permanently change the current drawing color in the Graphics page it has to set the page color to the color of the GeomShape object before it is drawn and to restore it to the original color afterwards. By defining an abstract method drawShape we can put all of the code for setting and restoring the color in draw and leave the question of how to actually draw the shape to a new abstract method. Once again, dynamic binding helps us. When the draw method calls drawShape, which class has its drawShape method called? As usual, it's the class of the object that draw was called on.

If a class declares one or more abstract methods, then the class itself must be declared abstract. However, you can still declare a class to be abstract even if it declares no abstract methods. As before, declaring a class to be abstract means that no object of the class can be created (even if the class contains no abstract methods).

A second abstract class, PointRelativeShape.java, is a subclass of GeomShape. This class is for shapes that have some "center" or reference point, where the shape is defined relative to that reference point.

PointRelativeShape takes care of defining the instance variables myX and myY, providing methods to return these values, and providing the move method. (Note that moving a point-relative shape requires moving the reference point.) It extends GeomShape. Even though it declares no methods as abstract, it fails to implement areaOf and drawShape, so it must be abstract as well.

Finally, Circle.java and Rect.java extend PointRelativeShape. Note the use of super in the constructors. The drawShape method uses getX and getY to figure out where the object is. They do not have to supply a move method, because they inherit it.

We can also have classes that inherit directly from GeomShape. Segment.java does so, and it implements everything itself.

To summarize, we have the following heirarchy of classes:

The superclass GeomShape is abstract; no objects of this class can be created. GeomShape serves as a placeholder in the inheritance hierarchy. Because it contains three abstract methods, drawShape, areaOf and move, any subclass that is instantiable (i.e., any subclass for which we can create objects) must have definitions of these methods. The GeomShape class does define an instance variable, myColor, along with a constructor, methods to set and get the color, and a stripped-down version of draw, which sets and restores the current color in the Graphics object while delegating the actual drawing of the shape to the abstract method drawShape.

The subclass PointRelativeShape is also abstract, though it does define two more instance variables, myX and myY, a constructor, getters for x and y values, and the move method.

The non-abstract classes Circle and Rect are subclasses of PointRelativeShape. Each has its own instance variables, constructor, draw method, and areaOf method. The drawShape methods are used in the draw method inherited froom GeomShape. The Circle class also includes a scale method.

The non-abstract class Segment is a direct subclass of GeomShape. It defines instance variables for the segment's endpoints, a constructor, and methods drawShape, move, and areaOf. Because Segment is a direct subclass of GeomShape, and because it's not abstract, it must define the move and areaOf methods on its own.

Here is a question to think about.

Now, let's look at the driver GeomShapeDriver.java, which demonstrates all of these classes, showing that everything works. Again, we see polymorphism in action. The variable shape is declared as a reference to GeomShape, but in reality it will refer to a Circle, Rect, or Segment object. The calls at the bottom of the loop body show dynamic binding in action. The first time through the loop, we call methods in the Circle class; the second time through, we call methods in the Rect class; and the third time through, we call methods in the Segment class. Note that the drawString calls continue to draw in black, even though shapes are drawn in various colors in between calls to drawString.

Note that in case 1, where shape actually references a Circle, we need to cast it as a reference to Circle before calling scale. That's because the scale method is not part of the GeomShape class.

What happens if we try to call scale on a Rect instead of a Circle? For example

GeomShape shape; // Reference to an object that can be a Circle, Rect, or Segment ... shape = new Rect(150, 200, 50, 25, Color.red); ((Circle) shape).scale(0.5); // This cast guaranteed to fail!! We get an exception at run time. A ClassCastException, to be precise.

Design Patterns

OO programming allows us to solve problems in ways that may not be obvious at first. But once an idea is seen in one program, it often can be used in other programs. The idea of doing all the work of saving the old color, changing the current color, and restoring the old color in the draw method of a superclass, while delegating the work of how to actually draw the shape to a call to an abstract method drawShape, is an instance of a more general pattern.

In fact, it is common enough that it is given a name: the Template Pattern. The template pattern says to create a "template" method in a superclass that does the basic work common to all subclasses and establishes the basic framework of what must be done in what order. The details of operations that will differ depending on the subclass are handled by defining an abstract method for each such operation. Alternately, a rudimentary "hook" method can be provided that does something simple (often nothing!), with the expectation that subclasses that want more complex operations will override this "hook" method. The subclasses provide implementations of all abstract methods and override "hook" methods as needed to customize the general template to their own purposes. (In our case, the subclasses define drawShape.)

There are whole books on design patterns in OO programming, the most famous of which is Design Patterns by Gamma, Helm, Johnson, and Vlissides. Identifying design patterns is an idea borrowed from architecture, and there are books on that, also. We won't be going into that much detail in this course, but when we use an idea that has been identified as a design pattern I will point out the pattern and tell you its name. That way when a similar pattern occurs in the future you will know at least one way to design a solution.

Adapter Classes

When we created listeners, we had to implement an interface. For the MouseListener interface, there were five methods that had to be implemented, and we usually used only one or two. We had to create empty-bodied implementations for the others. Having to do so was a minor irritant.

This situation comes up often enough that Java has provided an alternate approach. For each Listener interface, there is a corresponding Adapter class that supplies empty-body implementations for each of the methods in the interface. We then can extend these classes, overriding just the methods that we want to actually do something. We no longer have to supply the empty-body implementations, because the Adapter class has already done that, and we inherit them. (Gee, might this be an example of a Template Pattern, where the "hooks" are the empty-body methods?)

For example, consider DragAMac.java. It has two inner classes that implement the MouseListener and the MouseMotionListener interfaces. But instead of saying that we will implement these interfaces, we instead extend the MouseAdapter and MouseMotionAdapter classes. The empty-body implementations that we had before are now gone.

Note that if we use the applet itself as the listener (using this as the parameter to the appropriate addListener method calls), we cannot use this approach. That is because in creating the class DragAMac, we have already extended Applet, and Java does not allow multiple inheritance. In other words, Java does not allow a class to extend more than one other class. DragAMac has to extend Applet, and we can either implement the two interfaces directly or extend the Adapter classes in inner classes as we have done here. In fact, we had to use two different inner classes, since a single inner class cannot extend both Adapter classes.

We saw one other interface that we use to handle events: the ActionListener interface. Unlike MouseListener and MouseMotionListener, ActionListener has no corresponding adapter class. There's a pretty good reason for that. What do you think it is?

Inheritance vs. interfaces

The above point is worth repeating.
In Java, a subclass can extend just one superclass.
(Extending more than one superclass would be multiple inheritance.) For example, the following class declaration would not be legal: class Bozo extends Clown, TVStar { ... } On the other hand, a class is allowed to implement any number of interfaces. Thus, the following class declaration would be legal: class Bozo implements Clown, TVStar { ... }

Why doesn't Java allow multiple inheritance? Because it can cause problems that are difficult to resolve. What if more than one superclass defines the same instance variable that is visible to the subclass (i.e., either public or protected)? What if more than one superclass defines a method with the same name and signature? C++ allows multiple inheritance, and it pays the price in its complexity. The designers of Java decided to keep things simple, and so Java does not provide for multiple inheritance. Interfaces provide a way to get most of the benefits of multiple inheritance without most of the problems. The downside of interfaces, of course, is that they don't allow you to define instance variables in them, nor do they allow you to provide method bodies that are inherited.

Testing whether a cast will succeed

Recall from earlier that if a cast fails, we get an exception. Rather than just living with the run-time exception, there is a way for us to test at run time before making the cast whether it will succeed. It is the special Java operator instanceof. This operator takes a reference and a class, and it returns a boolean indicating whether the object referred to is an object of the given class or a subclass of that class. So in our GeomShapeDriver.java example, we could write GeomShape shape; ... shape = new Rect(150, 200, 50, 25, Color.red); if (shape instanceof Circle) ((Circle) shape).scale(0.5); The expression shape instanceof Circle evaluates to false, since Rect is not a subclass of Circle, and so the cast does not occur.

The ubiquitous superclass Object

In Java, there is a class that is a superclass of all other classes: Object. In other words, when we say public class Bozo { ... } that's just like saying public class Bozo extends Object { ... } Due to the subclass principle, you are allowed to use a reference to Object anywhere that you want to use a reference. Of course, you'll have to cast to a reference to the class that you are really using. You may recall that's exactly how an ArrayList works. If you could look at the source code for the ArrayList class, you would see that within an ArrayList object is an array declared like Object [] theList; Unless you used generic types, to use the ith element as a reference to an object of a class you have written, you'd have to cast.

There are several methods that every Object supports. Like any method of a superclass, if you don't override them in a class that you define, then you inherit the default version from Object. Of these inherited methods, the three most useful are toString, equals, and clone. We have already seen toString several times. The equals method has the following header:

boolean equals(Object obj) The default implementation of equals returns true if and only if the two object references are the same, i.e., if they reference the same object. (In other words, it's just like using ==.) But you can override the default definition as you like. For the BankAccount class, for example, you could write the method as public boolean equals(Object obj) { if (obj instanceof BankAccount) return balance == ((BankAccount) obj).balance; else return false; } This method considers two bank accounts to be "equal" if their balances are the same. We check that obj is an object of BankAccount or a subclass so that cast cannot cause an exception. If obj is not a BankAccount or a subclass, we just return false, since we clearly do not have two bank accounts, much less two "equal" bank accounts. This equals method would be called pretty much as you might expect: SavingsAccount momsSavings = new SavingsAccount(); CheckingAccount harrysChecking = new CheckingAccount(); boolean sameBalance = momsSavings.equals(harrysChecking);

The clone method makes a copy of an object, but there are some subtleties that make it tricky to use. In particular, if you copy references you get copies of the references, but the copies refer to the same objects as the original. This is called shallow copying. Deep copying, where new copies are made of any objects referenced, is sometimes what is desired. We won't cover the clone method in this course.

Swing vs AWT

The AWT (Abstract Windowing toolkit) was included in the original Java language. It was designed to be multiplatform, so that Windows, Mac, UNIX, and other operating systems would all be able to use it without modification. The idea was "write once, run anywhere." AWT was designed to use the buttons, windows, etc. that were supplied by the platform, however. GUIs that looked good on one platform would appear a bit different on other platforms, causing problems. The result was, "write once, debug everywhere."

Swing is a newer windowing toolkit originally introduced as a standard extension to Java 1.1 (thus the name javax.swing, with the "x" for extension). It became a standard part of Java 2, and it continues on in Java 5. Instead of using native buttons, etc., it paints its own, making things slower but more uniform across platforms.

We can use both AWT and Swing, giving us at least two ways to do pretty much everything. The Swing versions of classes start with a "J" (so that JButton is from Swing, whereas Button is from AWT). Swing is considered to be more powerful and flexible. If we want to use Swing components in a applet we should inherit from JApplet rather than Applet.

Unfortunately, even Swing is not quite "write once, run everywhere." Those of you using Mac OS X will quickly come to recognize the problem, because the Aqua interface overrides some of the properties that you try to assign to your GUI via Swing.

We will barely scratch the surface of AWT and Swing. They are huge, with hundreds of classes and more methods than anyone understands. Fortunately we can learn a few useful ones and several general principles which will let you learn new information as needed.


Back to syllabus
Scot Drysdale
Last modified: Sat Apr 26 2007