Polymorphism is the third essential feature
of an object-oriented programming language, after data abstraction and
inheritance.
It provides another dimension of separation of interface
from implementation, to decouple what from how.
Polymorphism allows improved code organization and
readability as well as the creation of extensible programs that can be
“grown” not only during the original creation of the project, but also when new
features are desired.
Encapsulation creates new data types by combining
characteristics and behaviors.
Implementation hiding separates the interface from the
implementation by making the details private.
This sort of mechanical organization makes ready
sense to someone with a procedural programming background.
But polymorphism deals
with decoupling in terms of types.
In Chapter 6 you saw how an object can be used as its own type or as an
object of its base type.
Taking an object reference and treating it as a reference
to its base type is called upcasting because of the way inheritance trees
are drawn with the base class at the top.
You also saw a problem arise, which
is embodied in the following example about musical instruments. Since several
examples play Notes, we should create the Note class separately,
in a package:
//: c07:music:Note.java // Notes to play on musical instruments.
public class Note {
private String noteName;
private Note(String noteName) {
this.noteName = noteName;
}
public String toString() { return noteName; }
public static final Note
MIDDLE_C = new Note("Middle C"),
C_SHARP = new Note("C Sharp"),
B_FLAT = new Note("B Flat");
// Etc. } ///:~
This is an “enumeration” class, which has a fixed number
of constant objects to choose from. You can’t make additional objects because
the constructor is private.
//:
Instrument.java
// Inheritance &
upcasting
class Instrument
{
public void
play() {}
static void tune( Instrument i ) {
//
...
i.play();
}
}
In the following example, Wind is a type of Instrument, therefore Wind is inherited from Instrument:
//: c07:music:Wind.java
// Wind objects are instruments // because they have the same interface: public class Wind extends Instrument {
// Redefine interface method: public void play(Note n) {
System.out.println("Wind.play() " + n);
}
} ///:~
//: c07:music:Music.java // Inheritance & upcasting.
public class Music {
public static void tune(Instrument i) {
// ... i.play(Note.MIDDLE_C); } public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting } } ///:~
The method Music.tune( ) accepts an
Instrument reference, but also anything derived from Instrument.
In main( ), you can see this happening as a
Wind reference is passed to tune( ), with no cast necessary.
This is acceptable—the interface in Instrument must exist in Wind, because Wind is inherited from Instrument.
Upcasting from Wind to Instrument may
“narrow” that interface, but it cannot make it anything less than the full
interface to Instrument.
Music.java might seem strange to you. Why should anyone intentionally forget the type of an object?
This is what happens when you upcast, and it seems like it
could be much more straightforward if tune( ) simply takes a
Wind reference as its argument.
This brings up an essential point: If you did that, you’d need to write a new
tune( ) for every type of Instrument in your system.
Suppose we follow this reasoning and add Stringed
and Brass instruments:
//: c07:music:Music2.java // Overloading instead of upcasting.
class Stringed extends Instrument {
public void play(Note n) {
System.out.println("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play(Note n) {
System.out.println("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // No upcasting tune(violin); tune(frenchHorn); } } ///:~
This works, but there’s a major drawback: you must write
type-specific methods for each new Instrument class you add.
This means more
programming in the first place, but it also means that if you want to add a new method like
tune( ) or a new type of Instrument, you’ve got a lot of work to do.
Add the fact that the compiler won’t give you any error
messages if you forget to overload one of your methods and the whole process of
working with types becomes unmanageable.
Wouldn’t it be much nicer
if you could just write a single method that takes the base class as its
argument, and not any of the specific derived classes?
That is, wouldn’t it be
nice if you could forget that there are derived classes, and write your code to
talk only to the base class?
That’s exactly what polymorphism allows you to do.
However, most programmers who come from a procedural
programming background have a bit of trouble with the way polymorphism works.
The difficulty with Music.java can be seen
by running the program. The output is Wind.play( ).
This is clearly the desired output, but it doesn’t seem to
make sense that it would work that way. Look at the tune( )
method:
public static void tune( Instrument i ) {
// ... i.play( Note.MIDDLE_C ); }
It receives an Instrument reference. So how can the
compiler possibly know that this Instrument reference points to a
Wind in this case and not a Brass or Stringed?
The compiler
can’t.
To get a deeper understanding of the issue, it’s helpful
to examine the subject of binding.
Connecting a method call to a method body is called
binding.
When binding is performed before the program is run (by the
compiler and linker, if there is one), it’s called early binding.
You might not have heard the term before because it has
never been an option with procedural languages. C compilers have only one kind
of method call, and that’s early binding.
The confusing part of the preceding program revolves
around early binding, because the compiler cannot know the correct method to
call when it has only an Instrument reference.
The solution is called
late binding, which means
that the binding occurs at run
time, based on the type of
object.
Late binding is also called dynamic binding or run-time binding.
When a language implements late binding, there must be some mechanism to
determine the type of the object at run time and to call the appropriate method.
That is, the compiler still doesn’t know the object type,
but the method-call mechanism finds out and calls the correct method body.
All method
binding in Java uses late binding unless the method is static or final (private methods are
implicitly final).
This means that ordinarily you don’t need to make any
decisions about whether late binding will occur—it happens automatically.
Once you know that all method binding in Java happens
polymorphically via late binding, you can write your code to talk to the base
class and know that all the derived-class cases will work correctly using the
same code.
Or to put it another way, you “send a message to an object
and let the object figure out the right thing to do.”
The classic example in OOP is the “shape” example.
The shape example has a base class
called Shape and various derived types: Circle, Square,
Triangle, etc.
The reason the example works so well is that it’s easy to
say “a circle is a type of shape” and be understood.
The upcast could occur in a statement as simple
as:
Shape s = new Circle();
Here, a Circle object is created, and the resulting
reference is immediately assigned to a Shape, which would seem to be an
error (assigning one type to another); and yet it’s fine because a Circle
is a Shape by inheritance. So the compiler agrees with the
statement and doesn’t issue an error message.
Suppose you call one of the base-class methods (that have
been overridden in the derived classes):
s.draw();
Again, you might expect that Shape’s
draw( ) is called because this is, after all, a Shape
reference—so how could the compiler know to do anything else?
And yet the proper Circle.draw( ) is called
because of late binding (polymorphism).
The following example puts it a slightly different
way:
//: c07:Shapes.java // Polymorphism in Java.
import java.util.*;
class Shape {
void draw() {}
void erase() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
System.out.println("Triangle.erase()");
}
}
// A "factory" that randomly creates shapes: class RandomShapeGenerator {
private Random rand = new Random();
public Shape next() {
switch( rand.nextInt(3) ) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
public class Shapes {
private static RandomShapeGenerator gen =
new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes: for(int i = 0; i < s.length; i++)
s[i] = gen.next();
// Make polymorphic method calls: for(int i = 0; i < s.length; i++)
s[i].draw();
}
} ///:~
The base class Shape establishes the common
interface to anything inherited from Shape—that is, all shapes can be drawn and erased.
The derived
classes override these definitions to provide unique behavior for each specific
type of shape.
RandomShapeGenerator is a kind of “factory” that
produces a reference to a randomly-selected Shape object each time you
call its next( ) method.
Note that the upcasting happens in the return
statements, each of which takes a reference to a Circle, Square,
or Triangle and sends it out of next( ) as the return type,
Shape.
So whenever you call next( ), you never get a
chance to see what specific type it is, since you always get back a plain
Shape reference.
The point of choosing the shapes randomly is to drive home
the understanding that the compiler can have no special knowledge that allows it
to make the correct calls at compile time. All the calls to draw( )
must be made through dynamic binding.
Now let’s return to the musical instrument example.
Because of
polymorphism, you can add as many new types as you want to the system without
changing the tune( ) method.
In a
well-designed OOP program, most or all of your methods will follow the model of
tune( ) and communicate only with the base-class
interface.
Such a program is extensible because you can add new
functionality by inheriting new data types from the common base class.
The methods that manipulate the base-class interface will
not need to be changed at all to accommodate the new classes.
Consider what happens if you take the instrument example
and add more methods in the base class and a number of new classes. Here’s the
diagram:
All these new classes work correctly with the old,
unchanged tune( ) method.
Even if tune( ) is in a separate file and new
methods are added to the interface of Instrument, tune( )
will still work correctly, even without recompiling it.
Here is the implementation of the diagram:
//: c07:music3:Music3.java
// An extensible program.
class Note {
private String noteName;
private Note(String noteName) {
this.noteName = noteName;
}
public String toString() { return noteName; }
public static final Note
MIDDLE_C = new Note("Middle C"),
C_SHARP = new Note("C Sharp"),
B_FLAT = new Note("B Flat");
// Etc.
} ///:~
class Instrument {
void play(Note n) {
System.out.println("Instrument.play() " + n);
}
String what() { return "Instrument"; }
void adjust() {}
}
class Wind extends Instrument {
void play(Note n) {
System.out.println("Wind.play() " + n);
}
String what() { return "Wind"; }
void adjust() {}
}
class Percussion extends Instrument {
void play(Note n) {
System.out.println("Percussion.play() " + n);
}
String what() { return "Percussion"; }
void adjust() {}
}
class Stringed extends Instrument {
void play(Note n) {
System.out.println("Stringed.play() " + n);
}
String what() { return "Stringed"; }
void adjust() {}
}
class Brass extends Wind {
void play(Note n) {
System.out.println("Brass.play() " + n);
}
void adjust() {
System.out.println("Brass.adjust()");
}
}
class Woodwind extends Wind {
void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn't care about type, so new types // added to the system still work right: public static void tune(Instrument i) {
// ... i.play(Note.MIDDLE_C); } public static void tuneAll(Instrument[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
// Upcasting during addition to the array: Instrument[] orchestra = { new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} ///:~
The new methods are what( ), which returns a
String reference with a description of the class, and
adjust( ), which provides some way to adjust each instrument.
In main( ), when you place something inside
the orchestra array, you automatically upcast to Instrument.
You can see that the tune( ) method is
blissfully ignorant of all the code changes that have happened around it, and
yet it works correctly.
This is exactly what
polymorphism is supposed to provide.
Changes in your code don’t cause damage to parts of the
program that should not be affected.
Put another way, polymorphism is an important technique
for the programmer to “separate the things that
change from the things that stay the same.”
In all the instrument examples, the methods in the base class Instrument were always “dummy” methods.
If these methods are ever
called, you’ve done something wrong.
That’s because the intent of
Instrument is to create a common interface
for all the classes derived from it.
The only reason to establish this common interface is so
it can be expressed differently for each different subtype.
It establishes a basic form, so you can say what’s in common with all the derived classes.
Another way of
saying this is to call Instrument an abstract base class (or simply an abstract class).
You create an abstract class when you want to manipulate a
set of classes through this common interface. All derived-class methods that
match the signature of the base-class declaration will be called using the
dynamic binding mechanism.
If you have an
abstract class like Instrument, objects of
that class almost always have no meaning.
That is, Instrument is meant to express only the
interface, and not a particular implementation, so creating an
Instrument object makes no sense, and you’ll probably want to prevent the
user from doing it.
Java provides a mechanism
for doing this called the abstract
method
This is a method that is incomplete; it has only a declaration and no method
body. Here is the syntax for an abstract method
declaration:
abstract void f();
A class containing
abstract methods is called an abstract
class.
If an abstract
class is incomplete, what is the compiler supposed to do when someone tries to make an object of that class?
Compiler cannot safely create an object of an abstract
class, so you get an error message from the compiler.
If you inherit from an
abstract class and you want to make objects of the new type, you must provide method definitions for all the abstract methods in the base
class.
It’s possible to create a class as abstract without
including any abstract methods. This is useful when you’ve got a class in
which it doesn’t make sense to have any abstract methods, and yet you
want to prevent any instances of that class.
The Instrument class can easily be turned into an
abstract class.
Only some of the methods will be abstract, since
making a class abstract doesn’t force you to make all the methods
abstract. Here’s what it looks like:
Here’s the orchestra example modified to use
abstract classes and methods:
//: c07:music4:Music4.java // Abstract classes and methods.
import java.util.*;
abstract class Instrument {
private int i; // Storage allocated for each public abstract void play(Note n);
public String what() {
return "Instrument";
}
public abstract void adjust();
}
class Wind extends Instrument {
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}
class Percussion extends Instrument {
public void play(Note n) {
System.out.println("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}
class Stringed extends Instrument {
public void play(Note n) {
System.out.println("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}
class Brass extends Wind {
public void play(Note n) {
System.out.println("Brass.play() " + n);
}
public void adjust() {
System.out.println("Brass.adjust()");
}
}
class Woodwind extends Wind {
public void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}
public class Music4 {
// Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument i) {
// ... i.play(Note.MIDDLE_C); } static void tuneAll(Instrument[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
// Upcasting during addition to the array: Instrument[] orchestra = { new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} ///:~
You can see that there’s
really no change except in the base class.
As usual, constructors are different from other kinds of
methods. This is also true when polymorphism is involved.
Even though constructors
are not polymorphic (they’re actually static methods, but the static declaration is
implicit), it’s important to understand the way constructors work in complex
hierarchies and with polymorphism.
The order of
constructor calls was briefly discussed in Chapter 4 and again in Chapter 6, but
that was before polymorphism was introduced.
The order of the
constructor calls is impo rtant.
When you inherit, you know all about the base class and
can access any public and protected members of the base class.
This means that you must be able to assume that all the
members of the base class are valid when you’re in the derived class.
In a normal method, construction has already taken place,
so all the members of all parts of the object have been built.
Inside the
constructor, however, you must be able to assume that all members that you use
have been built.
The only way to guarantee
this is for the base-class constructor to be called first.
When using composition and inheritance to create a new
class, most of the time you won’t have to worry about cleaning up; subobjects
can usually be left to the garbage collector. If you do have cleanup issues, you
must be diligent and create a dispose( ) method (the name I have
chosen to use here; you may come up with something better) for your new class.
And with inheritance, you must override dispose( ) in the derived class if you have any
special cleanup that must happen as part of garbage collection. When you
override dispose( ) in an inherited class, it’s important to
remember to call the base-class version of dispose( ), since
otherwise the base-class cleanup will not happen. The following example
demonstrates this:
//: c07:Frog.java // Cleanup and inheritance. import com.bruceeckel.simpletest.*;
class Characteristic {
private String s;
Characteristic(String s) {
this.s = s;
System.out.println("Creating Characteristic " + s);
}
protected void dispose() {
System.out.println("finalizing Characteristic " + s);
}
}
class Description {
private String s;
Description(String s) {
this.s = s;
System.out.println("Creating Description " + s);
}
protected void dispose() {
System.out.println("finalizing Description " + s);
}
}
class LivingCreature {
private Characteristic p = new Characteristic("is alive");
private Description t =
new Description("Basic Living Creature");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void dispose() {
System.out.println("LivingCreature dispose");
t.dispose();
p.dispose();
}
}
class Animal extends LivingCreature {
private Characteristic p= new Characteristic("has heart");
private Description t =
new Description("Animal not Vegetable");
Animal() {
System.out.println("Animal()");
}
protected void dispose() {
System.out.println("Animal dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
class Amphibian extends Animal {
private Characteristic p =
new Characteristic("can live in water");
private Description t =
new Description("Both water and land");
Amphibian() {
System.out.println("Amphibian()");
}
protected void dispose() {
System.out.println("Amphibian dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
public class Frog extends Amphibian {
private static Test monitor = new Test();
private Characteristic p = new Characteristic("Croaks");
private Description t = new Description("Eats Bugs");
public Frog() {
System.out.println("Frog()");
}
protected void dispose() {
System.out.println("Frog dispose");
t.dispose();
p.dispose();
super.dispose();
}
public static void main(String[] args) {
Frog frog = new Frog();
System.out.println("Bye!");
frog.dispose();
monitor.expect(new String[] {
"Creating Characteristic is alive",
"Creating Description Basic Living Creature",
"LivingCreature()",
"Creating Characteristic has heart",
"Creating Description Animal not Vegetable",
"Animal()",
"Creating Characteristic can live in water",
"Creating Description Both water and land",
"Amphibian()",
"Creating Characteristic Croaks",
"Creating Description Eats Bugs",
"Frog()",
"Bye!",
"Frog dispose",
"finalizing Description Eats Bugs",
"finalizing Characteristic Croaks",
"Amphibian dispose",
"finalizing Description Both water and land",
"finalizing Characteristic can live in water",
"Animal dispose",
"finalizing Description Animal not Vegetable",
"finalizing Characteristic has heart",
"LivingCreature dispose",
"finalizing Description Basic Living Creature",
"finalizing Characteristic is alive" }); } } ///:~
Each class in the hierarchy also contains a member objects
of types Characteristic and Description, which must also be
disposed. The order of disposal should be the reverse of the order of
initialization, in case one subobject is dependent on another. For fields, this
means the reverse of the order of declaration (since fields are initialized in
declaration order). For base classes (following the form that’s used in C++ for
destructors), you should perform the derived-class cleanup first, then the
base-class cleanup. That’s because the derived-class cleanup could call some
methods in the base class that require the base-class components to be alive, so
you must not destroy them prematurely. From the output you can see that all
parts of the Frog object are disposed in reverse order of creation.
From this example, you can see that although you don’t
always need to perform cleanup, when you do, the process requires care and
awareness.
The hierarchy of constructor calls brings up an interesting
dilemma.
What happens if
you’re inside a constructor and you call a dynamically-bound method of the
object being constructed?
Inside an
ordinary method, you can imagine what will happen: The dynamically-bound
call is resolved at run time, because the object cannot know whether it belongs
to the class that the method is in or some class derived from it. For
consistency, you might think this is what should happen inside constructors.
This is not
exactly the case.
If you call a
dynamically-bound method inside a constructor, the overridden definition for
that method is used.
However, the effect can be rather unexpected and
can conceal some difficult-to-find bugs.
Conceptually, the constructor’s job is to bring the object into existence (which is
hardly an ordinary feat).
Inside any constructor, the entire object might be only partially
formed—you can know only that the base-class objects have been
initialized, but you cannot know which classes are inherited from you.
A dynamically bound method call, however, reaches
“outward” into the inheritance hierarchy. It calls a method in a derived class.
If you do this inside a constructor, you call a method that might manipulate members that
haven’t been initialized yet—a sure recipe for disaster.
You can see the problem in the following
example:
//: c07:PolyConstructors.java // Constructors and polymorphism // don't produce what you might expect. import com.bruceeckel.simpletest.*;
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
private static Test monitor = new Test();
public static void main(String[] args) {
new RoundGlyph(5);
monitor.expect(new String[] {
"Glyph() before draw()",
"RoundGlyph.draw(), radius = 0",
"Glyph() after draw()",
"RoundGlyph.RoundGlyph(), radius = 5" }); } } ///:~
In Glyph, the draw( ) method is
abstract, so it is designed to be overridden. Indeed, you are forced to
override it in RoundGlyph.
But the Glyph constructor calls this method, and
the call ends up in RoundGlyph.draw( ), which would seem to be the
intent.
But if you look at the output, you can see that when
Glyph’s constructor calls draw( ), the value of radius
isn’t even the default initial value
1.
The order of initialization described in the earlier
section isn’t quite complete, and that’s the key to solving the mystery.
The actual
process of initialization is:
Once you learn
about polymorphism, it can seem that everything ought to be inherited, because
polymorphism is such a clever tool.
This can burden your designs; in fact, if you choose
inheritance first when you’re using an existing class to make a new class,
things can become needlessly complicated.
A better approach is
to choose composition first, especially when it’s not obvious which
one you should use.
Composition does not force a design into an inheritance
hierarchy.
But composition is also
more flexible since it’s possible to dynamically choose a type (and thus
behavior) when using composition, whereas inheritance requires an exact type to be known at
compile time.
The following example illustrates this:
//: c07:Transmogrify.java // Dynamically changing the behavior of an object // via composition (the "State" design pattern).
abstract class Actor {
public abstract void act();
}
class HappyActor extends Actor {
public void act() {
System.out.println("HappyActor");
}
}
class SadActor extends Actor {
public void act() {
System.out.println("SadActor");
}
}
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
} ///:~
A Stage object contains a reference to an
Actor, which is initialized to a HappyActor object. This means
performPlay( ) produces a particular behavior. But since a reference
can be rebound to a different object at run time, a reference for a
SadActor object can be substituted in actor, and then the behavior
produced by performPlay( ) changes. Thus you gain dynamic
flexibility at run time. (This is also called the State Pattern. See
Thinking in Patterns (with Java) at www.BruceEckel.com.) In
contrast, you can’t decide to inherit differently at run time; that must be
completely determined at compile time.
A general
guideline is “Use inheritance to express
differences in behavior, and fields to express variations in
state.”
In the preceding example, both are used; two different
classes are inherited to express the difference in the act( )
method, and Stage uses composition to allow its state to be
changed.
In this
case, that change in state happens to produce a change in behavior.
When studying inheritance, it would seem that the cleanest way
to create an inheritance hierarchy is to take the “pure” approach.
That is, only methods that have been established in the
base class or interface are to be overridden in the derived class, as
seen in this diagram:
This can be called a pure “is-a” relationship because the interface
of a class establishes what it is.
Inheritance guarantees that any derived class will have
the interface of the base class and nothing less.
If you follow this diagram, derived classes will also have
no more than the base-class interface.
This can be thought of as pure substitution, because derived class objects
can be perfectly substituted for the base class, and you never need to know any
extra information about the subclasses when you’re using them:
That is, the base class
can receive any message you can send to the derived class because the two have
exactly the same
interface.
All you need to do is upcast from the derived class and
never look back to see what exact type of object you’re dealing with. Everything
is handled through polymorphism.
When you see it this way,
it seems like a pure is-a relationship is the only sensible way to do things,
and any other design indicates muddled thinking and is by definition broken.
This too is a trap.
As soon as you start thinking this way, you’ll turn around
and discover that extending the interface (which, unfortunately, the keyword extends seems to encourage) is the
perfect solution to a particular problem.
This could be termed an “is-like-a” relationship, because the derived
class is like the base class—it has the same
fundamental interface—but it has other features that require additional methods
to implement:
While this is also a useful and sensible approach
(depending on the situation), it has a drawback.
The extended part of the
interface in the derived class is not available from the base class, so
once you upcast, you can’t call the new methods:
If you’re not upcasting in this case, it won’t bother you,
but often you’ll get into a situation in which you need to rediscover the exact
type of the object so you can access the extended methods of that type. The
following section shows how this is done.
Since you lose
the specific type information via an upcast (moving up the inheritance
hierarchy), it makes sense that to retrieve the type information—that is, to
move back down the inheritance hierarchy—you use a downcast.
However, you know an upcast is always safe; the base class
cannot have a bigger interface than the derived class.
Therefore, every message you send through the base class
interface is guaranteed to be accepted.
But with a downcast, you don’t really know that a shape
(for example) is actually a circle. It could instead be a triangle or square or
some other type.
To solve this problem,
there must be some way to guarantee that a downcast is correct, so that
you won’t accidentally cast to the wrong type and then send a message that the
object can’t accept. This would be quite unsafe.
In some languages (like C++) you must perform a special
operation in order to get a type-safe downcast, but in Java, every cast
is checked!
So even though it looks like you’re just performing an
ordinary parenthesized cast, at run time this cast is checked to ensure that it
is in fact the type you think it is. If it isn’t, you get a
ClassCastException.
This act of checking types at run time is called run-time
type identification (RTTI). The
following example demonstrates the behavior of RTTI:
//: c07:RTTI.java // Downcasting & Run-Time Type Identification (RTTI). // {ThrowsException} class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile time: method not found in Useful: //! x[1].u(); ((MoreUseful)x[1]).u(); // Downcast/RTTI ((MoreUseful)x[0]).u(); // Exception thrown } } ///:~
As in the diagram, MoreUseful extends the interface
of Useful. But since it’s inherited, it can also be upcast to a
Useful. You can see this happening in the initialization of the array
x in main( ). Since both objects in the array are of class
Useful, you can send the f( ) and g( ) methods to
both, and if you try to call u( ) (which exists only in
MoreUseful), you’ll get a compile-time error message.
If you want to access the extended interface of a
MoreUseful object, you can try to downcast. If it’s the correct type, it
will be successful. Otherwise, you’ll get a ClassCastException. You don’t need to write any special
code for this exception, since it indicates a programmer error that could happen
anywhere in a program.
There’s more to RTTI than a simple cast. For example,
there’s a way to see what type you’re dealing with before you try to
downcast it. All of Chapter 10 is devoted to the study of different aspects of
Java run-time type identification.
Polymorphism means “different forms.” In object-oriented
programming, you have the same face (the common interface in the base class) and
different forms using that face: the different versions of the dynamically bound
methods.
You’ve seen in this chapter that it’s
impossible to understand, or even create, an example of polymorphism without
using data abstraction and inheritance. Polymorphism is a feature that cannot be
viewed in isolation (like a switch statement can, for example), but
instead works only in concert, as part of a “big picture” of class
relationships. People are often confused by other, non-object-oriented features
of Java, like method overloading, which are sometimes presented as
object-oriented. Don’t be fooled: If it isn’t late binding, it isn’t
polymorphism.
To use polymorphism—and thus object-oriented
techniques—effectively in your programs, you must expand your view of
programming to include not just members and messages of an individual class, but
also the commonality among classes and their relationships with each other.
Although this requires significant effort, it’s a worthy struggle, because the
results are faster program development, better code organization, extensible
programs, and easier code maintenance.
Solutions to selected exercises can be found in the
electronic document The Thinking in Java Annotated Solution Guide,
available for a small fee from www.BruceEckel.com.
[32] For C++ programmers, this is the analogue of C++’s
pure virtual function.