Interchangeable
objects
with
polymorphism
Inheritance
usually ends up creating a family of classes, all based on the same uniform
interface. We express this with an inverted tree diagram:
[5] One
of the most important things you do with such a family of classes is to treat
an object of a derived class as an object of the base class. This is important
because it means you can write a single piece of code that ignores the specific
details of type and talks just to the base class. That code is then
decoupled
from type-specific information, and thus is simpler to write and easier to
understand. And, if a new type – a
Triangle,
for example –
is
added through inheritance, the code you write will work just as well for the
new type of
Shape
as it did on the existing types. Thus the program is
extensible. Consider
the above example. If you write a function in Java:
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}
This
function speaks to any
Shape,
so it is independent of the specific type of object it’s drawing and
erasing. If in some other program we use the
doStuff( )
function:
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
The
calls to
doStuff( )
automatically
work right, regardless of the exact type of the object.
This
is actually a pretty amazing trick. Consider the line:
What’s
happening here is that a
Circle
handle is being passed into a function that’s expecting a
Shape
handle. Since a
Circle
is
a
Shape
it can be treated as one by
doStuff( ).
That is, any message that
doStuff( )
can send to a
Shape,
a
Circle
can accept. So it is a completely safe and logical thing to do.
We
call this process of treating a derived type as though it were its base type
upcasting.
The name
cast
is
used in the sense of casting into a mold and the
up
comes from the way the inheritance diagram is typically arranged, with the base
type at the top and the derived classes fanning out downward. Thus, casting to
a base type is moving up the inheritance diagram: upcasting.
An
object-oriented program contains some upcasting somewhere, because that’s
how you decouple yourself from knowing about the exact type you’re
working with. Look at the code in
doStuff( ):
s.erase();
// ...
s.draw();
Notice
that it doesn’t say “If you’re a
Circle,
do this, if you’re a
Square,
do that, etc.” If you write that kind of code, which checks for all the
possible types a
Shape
can actually be, it’s messy and you need to change it every time you add
a new kind of
Shape.
Here, you just say “You’re a shape, I know you can
erase( )
yourself,
do it and take care of the details correctly.”
Dynamic
binding
What’s
amazing about the code in
doStuff( )
is that somehow the right thing happens. Calling
draw( )
for
Circle
causes different code to be executed than when calling
draw( )
for
a
Square
or a
Line,
but when the
draw( )
message is sent to an anonymous
Shape,
the correct behavior occurs based on the actual type that the
Shape
handle happens to be connected to. This is amazing because when the Java
compiler is compiling the code for
doStuff( ),
it cannot know exactly what types it is dealing with. So ordinarily,
you’d expect it to end up calling the version of
erase( )
for
Shape,
and
draw( )
for
Shape
and not for the specific
Circle,
Square,
or
Line.
And yet the right thing happens. Here’s how it works.
When
you send a message to an object even though you don’t know what specific
type it is, and the right thing happens, that’s called
polymorphism.
The process used by object-oriented programming languages to implement
polymorphism is called
dynamic
binding
.
The compiler and run-time system handle the details; all you need to know is
that it happens and more importantly how to design with it.
Some
languages require you to use a special keyword to enable dynamic binding. In
C++ this keyword is
virtual.
In Java, you never need to remember to add a keyword because functions are
automatically dynamically bound. So you can always expect that when you send a
message to an object, the object will do the right thing, even when upcasting
is involved.
Abstract
base classes and interfaces
Often
in a design, you want the base class to present
only
an interface for its derived classes. That is, you don’t want anyone to
actually create an object of the base class, only to upcast to it so that its
interface can be used. This is accomplished by making that class
abstract
using
the
abstract
keyword. If anyone tries to make an object of an
abstract
class, the compiler prevents them. This is a tool to enforce a particular design.
You
can also use the
abstract
keyword to describe a method that hasn’t been implemented yet – as
a stub indicating “here is an interface function for all types inherited
from this class, but at this point I don’t have any implementation for
it.” An
abstract
method
may be created only inside an
abstract
class.
When the class is inherited, that method must be implemented, or the inherited
class becomes
abstract
as well. Creating an
abstract
method allows you to put a method in an interface without being forced to
provide a possibly meaningless body of code for that method.
The
interface
keyword takes the concept of an
abstract
class one step further by preventing any function definitions at all. The
interface
is a very useful and commonly-used tool, as it provides the perfect separation
of interface and implementation. In addition, you can combine many interfaces
together, if you wish. (You cannot inherit from more than one regular
class
or
abstract
class
.)
[5]
This uses the
Unified
Notation
,
which will primarily be used in this book.