Objects, Inheritance and Polymorphism
Object Semantics
Suppose we have a simple class Foo:
class Foo {
private int theValue;
public Foo(int v) {theValue = v;}
public int getValue() {return theValue;}
public void setValue(int v) {theValue = v;}
}
Foo x = new Foo(3);
Foo y = new Foo(5);
x.setValue(2); // x is now 2
x = y;
// x is now 5
x.setValue(3); // x and y are
now 3. Why does y change?
It is important to
understand why y changes at the end. The local variables x and y do not
represent two different locations for the Foo objects; instead, they
represent pointers to the objects. After the line x=y, x and y
point to the same object.
Polymorphism
Here is a fundamental example about classes and so-called inheritance,
also in the demo example poly.zip. The
Child class "extends" the class Base. However, a Child object permanently
retains its identity, even when it is assigned to a Base variable (as in
b2 = new Child(4,5)). When the get() method is called, it is the Child get
method, not the Base get method.
using System;
using System.IO;
class Demo {
public static void main(String[] args) {
Base b1 = new Base(3);
Base b2 = new Child(4,5);
System.out.printf("b1.get() = %d, b2.get =%d%n", b1.get(), b2.get());
}
}
class Base {
private int x;
public int get() {return x;}
public Base(int X) {x = X;}
}
class Child extends Base {
private int y;
public int get() {return super.get()+y;}
public Child (int X, int Y) {super(X); y=Y;}
}
b1.get() returns 3; there is not much else it could return. However,
b2.get() returns 9. An observer who reasoned that b2 was of type Base might
try to invoke Base.get(), which would return 4.
The "polymorphism" behavior depends on get being declared in class
Base, but then "redeclared" in class Child. The most specific version
available at runtime is invoked; if this were not the case, then the call
to b2.get() would use "static" typing: b2 has type Base, so Base.get is
called, and the result would be 4, not 9. Static typing is not
what we want!
This behavior is sometimes also called "dynamic dispatch". When we call
b1.get(), the system at runtime looks up b1's "dispatch table" and invokes
the get() method present in that table. The table itself depends on how b1
was created: with new Base(3) or new Child(4,5).
Note also that there's a violation of one traditional type rule. A
Child object is not the same as a Base object, so how do
we allow assignments of the form Base = Child? And, if that, why do we not
allow Child = Base? The answer is that a Child "is a" Base in the sense
that a Child meets all the interface rules of a Base. In this
case, the Base class defines a get() method, and a Child object has a
get() method too, with the same semantics. A Child can be substituted, in
other words, anywhere the program expects a Base, with no possibility of
mismatch. The reverse is not true; a Child class could define a
new method not present in Base. (Note that if the Base class defines a
method not present in Child, then the Child simply inherits that method
unchanged.)
toString
System.out.println() is polymorphic: when you give it a non-string type,
it calls the object's toString() method to convert it to a string. We saw
this in the ratio.java demo.
Ratio.toString can be called explicitly whenever needed, as r.toString().
However, note that toString() works more generally, using inheritance.
The master parent class Object defines toString(); any subclass can override that definition, as is being
done here. In WriteLine(), when an Object needs to be printed then
toString() is called implicitly; the rules of inheritance ensure that the
most specific version of toString
is the one to be invoked.
Shapes
One classic application of this is with drawing shapes. There is a base
class Shape, and then child classes for each specific shape: Triangle,
Rectangle, Circle, etc. The base class has a draw() method, but it does
nothing (in fact, it is usually labeled abstract,
meaning it must be supplied by a subclass). If we have a list
shapeList of Shape objects (that is, a picture!) we can draw it as
follows:
for (Shape s : shapeList)
s.draw();
We do not have to check each shape s for what kind of
shape it is.
There are two issues here:
- The only benefit is being able to draw a shape without
checking what kind of shape it is.
- This is actually a very significant benefit. It means the program can
deal with shapes as atomic elements, and not have to analyze each shape
for its type.
GUIs
Suppose we want to assemble a graphical interface. We have a window w,
and a few buttons b1, b2 and b3, and some kind of slider control sc. How
can we organize all this?
One approach is to have a library routine clickHappened() that tells us
when a mouse event occurred, and where. If we figure out that button b1
was clicked, we look up the action that b1 was supposed to trigger, and
execute it. This will involve a lot of code like
if (b1
was clicked) execute_b1_action();
else if (b2 was clicked) execute_b2_action(); ...
But the object-oriented approach is very different:
- We have base classes for Window, Button and Slider
- We create our own child classes W, B1, B2, B3 and SC (capitalized
names of the individual objects)
- We now declare w of type W, b1 of type B1, etc
- Each child class has its own OnClick() method (or maybe OnDrag() for
the slider).
- The library framework is in charge of calling the OnClick() method of
any object clicked on
Now it all works! When b1 is clicked on, the framework invokes
B1.onClick() without any effort on our part.
Some variations allow us to pass an OnClick() method to each object
without actually defining an explicit child class:
Button savebutton = new Button(position,
save_file_method);
Zuul
The Game of Zuul came from a Java implementation in Objects First with Java
by David Barnes and Michael Kölling. Polymorphism is used extensively to
create different puzzle-room classes that behave differently from the
default Room class.
In the Game class, the main game loop is as follows:
while (! finished) {
Command command = parser.getCommand();
finished = processCommand(command);
}
The processCommand() function understands some universal commands such as
"quit", "help", "inventory" (listing the things you have with you) and
"look" (listing the things visible in the current room, although the room is
able to influence the visibility). But everything else is processed by the
room itself, via the Room.respond() method. A subclass of Room can in effect
redefine respond(), and thus create new behavior.
Overview of basic classes by file
Game.java: contains the main game loop, above
Parser.java, Command.java, commandWord.java: defines the basic mechanism for
reading commands
Item.java and Light.java: Items are things you can carry around with you,
like swords and flashlights. Some Items have the special property that they
can be "lighted"; some rooms (eg DarkRoom) won't show you whats' going on
unless you are carrying a light.
Room.java: Rooms have a description and a list of exits (north, south, east,
west, up, down, though others are possible). Rooms also have the following
virtual methods:
- getShortDescription()
- getExitString()
- getLongDescription() (a combination of the above)
- getExit(): for going to a new room
- respond(Command c, List<Item> inventory): how a Room responds to
a Command. The inventory is needed for TAKE and DROP commands, but also
to check if you are carrying a light or a key or a sword.
Each room contains a list of Items found in the room; Items are portable
and can be TAKEn or DROPped.
RoomMaker.java: creates the map of rooms. Technically this is called a directed
graph. (This isn't "just" a directed graph, or digraph; the
different links have names.)
Then there are the special rooms:
DarkRoom: if you don't have a light, only the way out is shown. No Items in
the room are visible. (And you cannot TAKE them if you guess their name)
DustyRoom: if you SWEEP, you find a WAND
LockedRoom: One exit is locked, and you can only go that way if you have a
key in your inventory. No "unlock" action is needed.
SerpentRoom: contains a giant but harmless serpent. He does block your way
back, though.
ValveRoom: You can't go DOWN unless you first TURN VALVE
ZooRoom: contains a sad zookeeper looking for a very small serpent
Downcasting
The player carries around a list of Items. How do we check this list to see
if one of the Items is a Light, and, if so, is actually Lighted? There is no
method in the Item class for asking if it is a Light, let alone the light's
status. Polymorphism, in other words, is no help to us. Polymorphism applies
only to methods that are part of the base class (and then redefined by the
derived class), not to methods (like Light.is_lighted()) that apply to the
The process of checking an object belonging to a base class (Item) to see if
it is in fact a member of a derived class, and, if so, viewing it as such,
is called downcasting.
The Light class has two static methods that do downcasting; here is
getLight() that returns the first lighted Light object on a
List<Item>:
public static Light getLight(List<Item> inv) {
int i = 0;
for (Item itm : inv) {
if (itm instanceof Light) {
Light lt = (Light) itm; // note (cast)
if (lt.isOn()) return lt;
}
}
return null; // get here if not found
}
Note the "if (itm instanceof Light)" which is a test for
whether an Item is a Light, and "Light lt = (Light) itm", which is the
actual conversion. If itm is not a Light, then the case would
throw an exception.
Shapesworld
Suppose we have the Shape class from earlier, and we want to go through
shapeList to find each Rectangle and modify it by rounding its corners with
the Rectangle-specific method setRoundCorners(). We can do the following:
for (Shape s :
ShapeList) {
if (s instanceof Rectangle) {
Rectangle r = (Rectangle) s;
r.setRoundCorners();
}
If all we wanted to do was draw the rectangles, we could use this; we don't
have to downcast as we're not actually calling any method that is not
present for the Shape class:
for (Shape s :
ShapeList) {
if (s instanceof Rectangle)
s.draw();
}
Commands and double dispatch
I want "turn valve" to work only in the Valveroom. However, it is then
important that "turn" be at least recognized everywhere, or else someone
might erroneously conclude that "turn" isn't a game command at all. In other
rooms, the response is "Hmm.. I don't seem to know how to turn here!" which
is a clue that "turn" might work somewhere else. How do I do this?
In the Room class, there is a catchall: if the verb word is legitimate (as
determined by the CommandWords class), then the Room.response() is "Hmm ...
I don't seem to know how to " + commandWord + "here!".
This means that each new verb must be entered into the CommandWords class.
It would also be possible to add to CommandWords a default response for each
verb to be used in rooms that didn't "understand" that verb. In this case we
might have a polymorphic verb.response() method; each verb would have its
own class and would have its own response().
This is double dispatch: we want response() to depend on
both the room and the verb,
polymorphically. There are languages that allow this directly, but Java (and
most OO languages, in fact) are a little more constrained. If we took that
route, we would first try room.response(verb) and then, if that "failed",
would try verb.response() (probably built into the default Room.response().