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.
Segment as a subclass
of PointRelativeShape. How would we do so? Would
this be a good idea?
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
ClassCastException,
to be precise.
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.
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?
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:
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.
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
shape instanceof Circle evaluates to
false, since Rect is not a subclass of
Circle, and so the cast does not occur.
ObjectObject. In other words, when we say
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
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:
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
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:
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 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.