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 the last chapter,
you saw how inheritance allows the treatment of an object
as its own type or its base type. This ability is critical because it
allows many types (derived from the same base type) to be treated as if they
were one type, and a single piece of code to work on all those different types
equally. The polymorphic method call allows one type to
express its distinction from another, similar type, as long as they’re
both derived from the same base type. This distinction is expressed through
differences in behavior of the methods that you can call through the base
class.
In this chapter, you’ll learn about
polymorphism (also called
dynamic
binding or late binding or run-time binding) starting
from the basics, with simple examples that strip away everything but the
polymorphic behavior of the
program.
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:
//: c07:music:Music.java // Inheritance & upcasting. class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // Etc. class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } } // Wind objects are instruments // because they have the same interface: class Wind extends Instrument { // Redefine interface method: public void play(Note n) { System.out.println("Wind.play()"); } } 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.
This program 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:music2:Music2.java // Overloading instead of upcasting. class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // Etc. class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } } class Wind extends Instrument { public void play(Note n) { System.out.println("Wind.play()"); } } class Stringed extends Instrument { public void play(Note n) { System.out.println("Stringed.play()"); } } class Brass extends Instrument { public void play(Note n) { System.out.println("Brass.play()"); } } 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 above 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. The late-binding mechanism varies
from language to language, but you can imagine that some sort of type
information must be installed in the objects.
All method binding in Java uses late
binding unless a method has been declared
final. This means that
ordinarily you don’t need to make any decisions about whether late binding
will occur—it happens automatically.
Why would you declare a method
final? As noted in the last chapter, it prevents anyone from overriding
that method. Perhaps more important, it effectively “turns off”
dynamic binding, or rather it tells the compiler that dynamic binding
isn’t necessary. This allows the compiler to generate slightly more
efficient code for final method calls. However, in most cases it
won’t make any overall performance difference in your program, so
it’s best to only use final as a design decision, and not as an
attempt to improve
performance.
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. This is commonly used
because it is easy to visualize, but unfortunately it can confuse novice
programmers into thinking that OOP is just for graphics programming, which is of
course not the case.
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 inheritance diagram shows the relationships:
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. 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()"); } } public class Shapes { public static Shape randShape() { switch((int)(Math.random() * 3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } } 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] = randShape(); // 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.
The main class Shapes contains a
static method randShape( ) that produces a reference to a
randomly-selected Shape object each time you call it. Note that the
upcasting happens in each of the return statements, which take a
reference to a Circle, Square, or Triangle and sends it out
of the method as the return type, Shape. So whenever you call this method
you never get a chance to see what specific type it is, since you always get
back a plain Shape reference.
main( ) contains an array of
Shape references filled through calls to randShape( ). At
this point you know you have Shapes, but you don’t know anything
more specific than that (and neither does the compiler). However, when you step
through this array and call draw( ) for each one, the correct
type-specific behavior magically occurs, as you can see from one output
example:
Circle.draw() Triangle.draw() Circle.draw() Circle.draw() Circle.draw() Square.draw() Triangle.draw() Square.draw() Square.draw()
Of course, since the shapes are all
chosen randomly each time, your runs will have different results. 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( ) are 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( ) works correctly without recompilation. Here is the
implementation of the above diagram:
//: c07:music3:Music3.java // An extensible program. import java.util.*; class Instrument { public void play() { System.out.println("Instrument.play()"); } public String what() { return "Instrument"; } public void adjust() {} } class Wind extends Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} } class Percussion extends Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} } class Stringed extends Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} } class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } } class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } public String what() { return "Woodwind"; } } public class Music3 { // Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument i) { // ... i.play(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = 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 Instrument 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. Your code changes don’t cause damage to parts of the program that
should not be affected. Put another way, polymorphism is one of the most
important techniques that allow the programmer to “separate the things
that change from the things that stay the
same.”
Let’s take a different look at the
first example in this chapter. In the following program, the interface of the
method play( ) is changed in the process of overriding it, which
means that you haven’t overridden the method, but instead
overloaded it. The compiler allows you to overload methods so it gives no
complaint. But the behavior is probably not what you want. Here’s the
example:
//: c07:WindError.java // Accidentally changing the interface. class NoteX { public static final int MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2; } class InstrumentX { public void play(int NoteX) { System.out.println("InstrumentX.play()"); } } class WindX extends InstrumentX { // OOPS! Changes the method interface: public void play(NoteX n) { System.out.println("WindX.play(NoteX n)"); } } public class WindError { public static void tune(InstrumentX i) { // ... i.play(NoteX.MIDDLE_C); } public static void main(String[] args) { WindX flute = new WindX(); tune(flute); // Not the desired behavior! } } ///:~
There’s another confusing aspect
thrown in here. In InstrumentX, the play( ) method takes an
int that has the identifier NoteX. That is, even though
NoteX is a class name, it can also be used as an identifier without
complaint. But in WindX, play( ) takes a NoteX
reference that has an identifier n. (Although you could even say
play(NoteX NoteX) without an error.) Thus it appears that the programmer
intended to override play( ) but mistyped the method a bit. The
compiler, however, assumed that an overload and not an override was intended.
Note that if you follow the standard Java naming convention, the argument
identifier would be noteX (lowercase ‘n’), which would
distinguish it from the class name.
In tune, the InstrumentX
i is sent the play( ) message, with one of
NoteX’s members (MIDDLE_C) as an argument. Since
NoteX contains int definitions, this means that the int
version of the now-overloaded play( ) method is called, and since
that has not been overridden the base-class version is
used.
The output is:
InstrumentX.play()
This certainly doesn’t appear to be
a polymorphic method call. Once you understand what’s happening, you can
fix the problem fairly easily, but imagine how difficult it might be to find the
bug if it’s buried in a program of significant
size.
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. (However, as seen in the last section, if
the method’s name is the same as the base class but the arguments are
different, you’ve got overloading, which probably isn’t what you
want.)
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. This can be
accomplished by making all the methods in Instrument print error
messages, but that delays the information until run-time and requires reliable
exhaustive testing on the user’s part. It’s always better to catch
problems at compile-time.
Java provides a mechanism for doing this
called the abstract
method[38].
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 a class contains one or more abstract
methods, the class must be qualified as abstract. (Otherwise, the
compiler gives you an error message.)
If an abstract class is incomplete, what
is the compiler supposed to do when someone tries to make an object of that
class? It cannot safely create an object of an abstract class, so you get an
error message from the compiler. This way the compiler ensures the purity of the
abstract class, and you don’t need to worry about misusing
it.
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. If
you don’t (and you may choose not to), then the derived class is also
abstract and the compiler will force you to qualify that class with the
abstract keyword.
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 { int i; // storage allocated for each public abstract void play(); public String what() { return "Instrument"; } public abstract void adjust(); } class Wind extends Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} } class Percussion extends Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} } class Stringed extends Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} } class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } } class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } 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(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = new Woodwind(); tuneAll(orchestra); } } ///:~
You can see that there’s really no
change except in the base class.
It’s helpful to create abstract
classes and methods because they make the abstractness of a class explicit,
and tell both the user and the compiler how it was
intended to be
used.
As usual,
constructors are different from
other kinds of methods. This is also true when polymorphism is involved. Even
though constructors are not polymorphic (although you can have a kind of
“virtual constructor,” as you will see in Chapter 12), it’s
important to understand the way constructors work in complex hierarchies and
with polymorphism. This understanding will help you avoid unpleasant
entanglements.
The order of constructor calls was
briefly discussed in Chapter 4 and again in Chapter 6, but that was before
polymorphism was introduced.
A constructor for the base class is
always called in the constructor for a derived class, chaining up the
inheritance hierarchy so that a constructor for every base class is called. This
makes sense because the constructor has a special job: to see that the object is
built properly. A derived class has access to its own members only, and not to
those of the base class (whose members are typically private). Only the
base-class constructor has the proper knowledge and access to initialize its own
elements. Therefore, it’s essential that all constructors get called,
otherwise the entire object wouldn’t be constructed. That’s why the
compiler enforces a constructor call for every portion of a derived class. It
will silently call the default constructor if you don’t explicitly call a
base-class constructor in the derived-class constructor body. If there is no
default constructor, the compiler will complain. (In the case where a class has
no constructors, the compiler will automatically synthesize a default
constructor.)
Let’s take a look at an example
that shows the effects of composition, inheritance, and polymorphism on the
order of construction:
//: c07:Sandwich.java // Order of constructor calls. class Meal { Meal() { System.out.println("Meal()"); } } class Bread { Bread() { System.out.println("Bread()"); } } class Cheese { Cheese() { System.out.println("Cheese()"); } } class Lettuce { Lettuce() { System.out.println("Lettuce()"); } } class Lunch extends Meal { Lunch() { System.out.println("Lunch()");} } class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } } class Sandwich extends PortableLunch { Bread b = new Bread(); Cheese c = new Cheese(); Lettuce l = new Lettuce(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ///:~
This example creates a complex class out
of other classes, and each class has a constructor that announces itself. The
important class is Sandwich, which reflects three levels of inheritance
(four, if you count the implicit inheritance from Object) and three
member objects. When a Sandwich object is created in main( ),
the output is:
Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich()
The order of the
constructor calls is important. 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. Then when
you’re in the derived-class constructor, all the members you can access in
the base class have been initialized. “Knowing that all members are
valid” inside the constructor is also the reason that, whenever possible,
you should initialize all member objects (that is, objects placed in the class
using composition) at their point of definition in the class (e.g., b,
c, and l in the example above). If you follow this practice, you
will help ensure that all base class members and member objects of the
current object have been initialized. Unfortunately, this doesn’t handle
every case, as you will see in the next
section.
When you use composition to create a new
class, you never worry about finalizing the member objects of that class. Each
member is an independent object, and thus is garbage
collected and finalized regardless of whether it happens to be a member of your
class. With inheritance, however, you must override
finalize( ) in the
derived class if you have any special cleanup that must happen as part of
garbage collection. When you override finalize( ) in an inherited
class, it’s important to remember to call the base-class version of
finalize( ), since otherwise the base-class finalization will not
happen. The following example proves this:
//: c07:Frog.java // Testing finalize with inheritance. class DoBaseFinalization { public static boolean flag = false; } class Characteristic { String s; Characteristic(String c) { s = c; System.out.println( "Creating Characteristic " + s); } protected void finalize() { System.out.println( "finalizing Characteristic " + s); } } class LivingCreature { Characteristic p = new Characteristic("is alive"); LivingCreature() { System.out.println("LivingCreature()"); } protected void finalize() { System.out.println( "LivingCreature finalize"); // Call base-class version LAST! if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } class Animal extends LivingCreature { Characteristic p = new Characteristic("has heart"); Animal() { System.out.println("Animal()"); } protected void finalize() { System.out.println("Animal finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } class Amphibian extends Animal { Characteristic p = new Characteristic("can live in water"); Amphibian() { System.out.println("Amphibian()"); } protected void finalize() { System.out.println("Amphibian finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } public class Frog extends Amphibian { Frog() { System.out.println("Frog()"); } protected void finalize() { System.out.println("Frog finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } public static void main(String[] args) { if(args.length != 0 && args[0].equals("finalize")) DoBaseFinalization.flag = true; else System.out.println("not finalizing bases"); new Frog(); // Instantly becomes garbage System.out.println("bye!"); // Force finalizers to be called: System.gc(); } } ///:~
The class DoBaseFinalization
simply holds a flag that indicates to each class in the hierarchy whether to
call
super.finalize( ).
This flag is set based on a command-line argument, so you can view the behavior
with and without base-class finalization.
Each class in the hierarchy also contains
a member object of class Characteristic. You will see that regardless of
whether the base class finalizers are called, the Characteristic member
objects are always finalized.
Each overridden finalize( )
must have access to at least protected members since the
finalize( ) method in class Object is protected and
the compiler will not allow you to reduce the access during inheritance.
(“Friendly” is less
accessible than protected.)
In Frog.main( ), the
DoBaseFinalization flag is configured and a single Frog object is
created. Remember that garbage collection—and in particular
finalization—might not happen for any particular object, so to enforce
this, the call to System.gc( ) triggers
garbage collection, and thus finalization. Without base-class finalization, the
output is:
not finalizing bases Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() bye! Frog finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water
You can see that, indeed, no finalizers
are called for the base classes of Frog (the member objects are
finalized, as you would expect). But if you add the “finalize”
argument on the command line, you get:
Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() bye! Frog finalize Amphibian finalize Animal finalize LivingCreature finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water
Although the order the member objects are
finalized is the same order that they are created, technically the
order of
finalization of objects is unspecified. With base classes, however, you have
control over the order of finalization. The best order to use is the one
that’s shown here, which is the reverse of the order of initialization.
Following the form that’s used in C++ for destructors, you should perform
the derived-class finalization first, then the base-class finalization.
That’s because the derived-class finalization could call some methods in
the base class that require that the base-class components are still alive, so
you must not destroy them
prematurely.
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. abstract class Glyph { abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { 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 { public static void main(String[] args) { new RoundGlyph(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 look at the output:
Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5
When Glyph’s constructor
calls draw( ), the value of radius isn’t even the
default initial value 1. It’s 0. This would probably result in either a
dot or nothing at all being drawn on the screen, and you’d be left
staring, trying to figure out why the program won’t work.
The
order of initialization described
in the previous section isn’t quite complete, and that’s the key to
solving the mystery. The actual process of initialization is:
There’s an
upside to this, which is that everything is at least initialized to zero (or
whatever zero means for that particular data type) and not just left as garbage.
This includes object references that are embedded inside a class via
composition, which become null. So if you forget to initialize that
reference you’ll get an exception at run-time. Everything else gets zero,
which is usually a telltale value when looking at output.
On the other hand, you should be pretty
horrified at the outcome of this program. You’ve done a perfectly logical
thing, and yet the behavior is mysteriously wrong, with no complaints from the
compiler. (C++ produces more rational behavior in this situation.) Bugs like
this could easily be buried and take a long time to discover.
As a result, a good guideline for
constructors is, “Do as little as possible to set the object into a good
state, and if you can possibly avoid it, don’t call any methods.”
The only safe methods to call inside a constructor are those that are
final in the base class. (This also applies to
private
methods, which are automatically final.) These cannot be overridden and
thus cannot produce this kind of
surprise.
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, 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. abstract class Actor { 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 { Actor a = new HappyActor(); void change() { a = new SadActor(); } void go() { a.act(); } } public class Transmogrify { public static void main(String[] args) { Stage s = new Stage(); s.go(); // Prints "HappyActor" s.change(); s.go(); // Prints "SadActor" } } ///:~
A Stage object contains a
reference to an Actor, which is initialized to a HappyActor
object. This means go( ) 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 a and then the behavior
produced by go( ) changes. Thus you gain dynamic flexibility at
run-time. (This is also called the State Pattern. See Thinking in
Patterns with Java, downloadable 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 above 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 termed 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 the
above 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 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). import java.util.*; 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 12 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.
[38]
For C++ programmers, this is the analogue of C++’s pure virtual
function.