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 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
handle and treating it as the handle of the 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:
//: Music.java // Inheritance & upcasting package c07; class Note { private int value; private Note(int val) { value = val; } public static final Note middleC = new Note(0), cSharp = new Note(1), cFlat = 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.middleC); } public static void main(String[] args) { Wind flute = new Wind(); tune(flute); // Upcasting } } ///:~
The method
Music.tune( ) accepts an Instrument handle, but also anything
derived from Instrument. In main( ), you can see this
happening as a Wind handle 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 handle 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:
//: Music2.java // Overloading instead of upcasting class Note2 { private int value; private Note2(int val) { value = val; } public static final Note2 middleC = new Note2(0), cSharp = new Note2(1), cFlat = new Note2(2); } // Etc. class Instrument2 { public void play(Note2 n) { System.out.println("Instrument2.play()"); } } class Wind2 extends Instrument2 { public void play(Note2 n) { System.out.println("Wind2.play()"); } } class Stringed2 extends Instrument2 { public void play(Note2 n) { System.out.println("Stringed2.play()"); } } class Brass2 extends Instrument2 { public void play(Note2 n) { System.out.println("Brass2.play()"); } } public class Music2 { public static void tune(Wind2 i) { i.play(Note2.middleC); } public static void tune(Stringed2 i) { i.play(Note2.middleC); } public static void tune(Brass2 i) { i.play(Note2.middleC); } public static void main(String[] args) { Wind2 flute = new Wind2(); Stringed2 violin = new Stringed2(); Brass2 frenchHorn = new Brass2(); 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
Instrument2 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.middleC); }
It receives an Instrument
handle. So how can the compiler possibly know that this Instrument handle
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
useful 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
handle.
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 you
ordinarily 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 importantly, it effectively “turns off”
dynamic binding, or rather it tells the compiler that dynamic binding
isn’t necessary. This allows the compiler to generate more efficient code
for final method
calls.
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 handle 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.
When 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 handle, 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:
//: 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: // To quiet the compiler 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 handle
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 handle
to a Circle, Square, or Triangle and send 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 handle.
main( ) contains an
array of Shape handles 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:
//: Music3.java // An extensible program import java.util.*; class Instrument3 { public void play() { System.out.println("Instrument3.play()"); } public String what() { return "Instrument3"; } public void adjust() {} } class Wind3 extends Instrument3 { public void play() { System.out.println("Wind3.play()"); } public String what() { return "Wind3"; } public void adjust() {} } class Percussion3 extends Instrument3 { public void play() { System.out.println("Percussion3.play()"); } public String what() { return "Percussion3"; } public void adjust() {} } class Stringed3 extends Instrument3 { public void play() { System.out.println("Stringed3.play()"); } public String what() { return "Stringed3"; } public void adjust() {} } class Brass3 extends Wind3 { public void play() { System.out.println("Brass3.play()"); } public void adjust() { System.out.println("Brass3.adjust()"); } } class Woodwind3 extends Wind3 { public void play() { System.out.println("Woodwind3.play()"); } public String what() { return "Woodwind3"; } } public class Music3 { // Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument3 i) { // ... i.play(); } static void tuneAll(Instrument3[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument3[] orchestra = new Instrument3[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind3(); orchestra[i++] = new Percussion3(); orchestra[i++] = new Stringed3(); orchestra[i++] = new Brass3(); orchestra[i++] = new Woodwind3(); tuneAll(orchestra); } } ///:~
The new methods are
what( ), which returns a String handle with a description of
the class, and adjust( ), which provides some way to adjust each
instrument.
In main( ), when you
place something inside the Instrument3 array you automatically upcast to
Instrument3.
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:
//: 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 handle
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, 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 this 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. 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
X();
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 declare 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:
//: Music4.java // Abstract classes and methods import java.util.*; abstract class Instrument4 { int i; // storage allocated for each public abstract void play(); public String what() { return "Instrument4"; } public abstract void adjust(); } class Wind4 extends Instrument4 { public void play() { System.out.println("Wind4.play()"); } public String what() { return "Wind4"; } public void adjust() {} } class Percussion4 extends Instrument4 { public void play() { System.out.println("Percussion4.play()"); } public String what() { return "Percussion4"; } public void adjust() {} } class Stringed4 extends Instrument4 { public void play() { System.out.println("Stringed4.play()"); } public String what() { return "Stringed4"; } public void adjust() {} } class Brass4 extends Wind4 { public void play() { System.out.println("Brass4.play()"); } public void adjust() { System.out.println("Brass4.adjust()"); } } class Woodwind4 extends Wind4 { public void play() { System.out.println("Woodwind4.play()"); } public String what() { return "Woodwind4"; } } public class Music4 { // Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument4 i) { // ... i.play(); } static void tuneAll(Instrument4[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument4[] orchestra = new Instrument4[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind4(); orchestra[i++] = new Percussion4(); orchestra[i++] = new Stringed4(); orchestra[i++] = new Brass4(); orchestra[i++] = new Woodwind4(); 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.
The
interface keyword takes the abstract
concept one step further. You could think of it as a “pure”
abstract class. It allows the creator to establish the form for a class:
method names, argument lists and return types, but no method bodies. An
interface can also contain data members of primitive types, but these are
implicitly static and
final. An interface provides only a form,
but no implementation.
An interface says:
“This is what all classes that implement this particular interface
will look like.” Thus, any code that uses a particular interface
knows what methods might be called for that interface, and that’s
all. So the interface is used to establish a “protocol”
between classes. (Some object-oriented programming languages have a keyword
called protocol to do the
same thing.)
To create an interface, use
the interface keyword instead of the class keyword. Like a class,
you can add the public keyword before the
interface keyword (but only if that interface is defined in a file
of the same name) or leave it off to give
“friendly” status.
To make a class that conforms to a
particular interface (or group of interfaces) use the
implements keyword. You’re saying “The
interface is what it looks like and here’s how it
works.” Other than that, it bears a strong resemblance to
inheritance. The diagram for the instrument example shows this:
Once you’ve implemented an
interface, that implementation becomes an ordinary class that can be
extended in the regular way.
You can choose to explicitly
declare the method declarations in an interface as public. But
they are public even if you don’t say it. So when you
implement an interface, the methods from the interface must
be defined as public. Otherwise they would default to
“friendly” and you’d be restricting the accessibility of a
method during inheritance, which is not allowed by the Java
compiler.
You can see this in the modified
version of the Instrument example. Note that every method in the
interface is strictly a declaration, which is the only thing the compiler
allows. In addition, none of the methods in Instrument5 are declared as
public, but they’re automatically public
anyway:
//: Music5.java // Interfaces import java.util.*; interface Instrument5 { // Compile-time constant: int i = 5; // static & final // Cannot have method definitions: void play(); // Automatically public String what(); void adjust(); } class Wind5 implements Instrument5 { public void play() { System.out.println("Wind5.play()"); } public String what() { return "Wind5"; } public void adjust() {} } class Percussion5 implements Instrument5 { public void play() { System.out.println("Percussion5.play()"); } public String what() { return "Percussion5"; } public void adjust() {} } class Stringed5 implements Instrument5 { public void play() { System.out.println("Stringed5.play()"); } public String what() { return "Stringed5"; } public void adjust() {} } class Brass5 extends Wind5 { public void play() { System.out.println("Brass5.play()"); } public void adjust() { System.out.println("Brass5.adjust()"); } } class Woodwind5 extends Wind5 { public void play() { System.out.println("Woodwind5.play()"); } public String what() { return "Woodwind5"; } } public class Music5 { // Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument5 i) { // ... i.play(); } static void tuneAll(Instrument5[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument5[] orchestra = new Instrument5[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind5(); orchestra[i++] = new Percussion5(); orchestra[i++] = new Stringed5(); orchestra[i++] = new Brass5(); orchestra[i++] = new Woodwind5(); tuneAll(orchestra); } } ///:~
The rest of the code works the
same. It doesn’t matter if you are upcasting to a
“regular” class called Instrument5, an abstract class
called Instrument5, or to an interface
called Instrument5. The behavior is the same. In fact, you can see in the
tune( ) method that there isn’t any evidence about whether
Instrument5 is a “regular” class, an abstract class or
an interface. This is the intent: Each approach gives the programmer
different control over the way objects are created and
used.
The interface isn’t
simply a “more pure” form of abstract class. It has a higher
purpose than that. Because an interface has no implementation at all
– that is, there is no storage associated with an interface
– there’s nothing to prevent many interfaces from
being combined. This is valuable because there are times when you need to say
“An x is an a and a b and a
c.” In C++, this act of combining multiple class interfaces is
called multiple
inheritance, and it carries some rather sticky baggage because each class
can have an implementation. In Java, you can perform the same act, but only one
of the classes can have an implementation, so the problems seen in C++ do not
occur with Java when combining multiple interfaces:
In a derived class, you
aren’t forced to have a base class that is either an abstract or
“concrete” (one with no abstract methods). If you do
inherit from a non-interface, you can inherit from only one. All
the rest of the base elements must be interfaces. You place all the
interface names after the implements keyword and separate them with
commas. You can have as many interfaces as you want and each one becomes
an independent type that you can upcast to. The following example shows a
concrete class combined with several interfaces to produce a new
class:
//: Adventure.java // Multiple interfaces import java.util.*; interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() {} } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {} public void fly() {} } public class Adventure { static void t(CanFight x) { x.fight(); } static void u(CanSwim x) { x.swim(); } static void v(CanFly x) { x.fly(); } static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero i = new Hero(); t(i); // Treat it as a CanFight u(i); // Treat it as a CanSwim v(i); // Treat it as a CanFly w(i); // Treat it as an ActionCharacter } } ///:~
You can see that Hero
combines the concrete class ActionCharacter with the interfaces
CanFight, CanSwim, and CanFly. When you combine a concrete
class with interfaces this way, the concrete class must come first, then the
interfaces. (The compiler gives an error otherwise.)
Note that the signature for
fight( ) is the same in the interface CanFight and the class
ActionCharacter, and that fight( ) is not provided
with a definition in Hero. The rule for an interface is that you
can inherit from it (as you will see shortly), but then you’ve got another
interface. If you want to create an object of the new type, it must be a
class with all definitions provided. Even though Hero does not explicitly
provide a definition for fight( ), the definition comes along with
ActionCharacter so it is automatically provided and it’s possible
to create objects of Hero.
In class Adventure, you can
see that there are four methods that take as arguments the various interfaces
and the concrete class. When a Hero object is created, it can be passed
to any of these methods, which means it is being upcast to each interface
in turn. Because of the way interfaces are designed in Java, this works without
a hitch and without any particular effort on the part of the
programmer.
Keep in mind that the core reason
for interfaces is shown in the above example: to be able to upcast to more than
one base type. However, a second reason for using interfaces is the same as
using an abstract base class: to prevent the client programmer from
making an object of this class and to establish that it is only an interface.
This brings up a question: Should you use an
interface or an
abstract class? An interface gives you the benefits of an
abstract class and the benefits of an interface, so if
it’s possible to create your base class without any method definitions or
member variables you should always prefer interfaces to abstract
classes. In fact, if you know something is going to be a base class, your first
choice should be to make it an interface, and only if you’re forced
to have method definitions or member variables should you change to an
abstract
class.
You can easily add new method
declarations to an
interface using
inheritance, and you can also combine several interfaces into a new
interface with inheritance. In both cases you get a new interface,
as seen in this example:
//: HorrorShow.java // Extending an interface with inheritance interface Monster { void menace(); } interface DangerousMonster extends Monster { void destroy(); } interface Lethal { void kill(); } class DragonZilla implements DangerousMonster { public void menace() {} public void destroy() {} } interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); } class HorrorShow { static void u(Monster b) { b.menace(); } static void v(DangerousMonster d) { d.menace(); d.destroy(); } public static void main(String[] args) { DragonZilla if2 = new DragonZilla(); u(if2); v(if2); } } ///:~
DangerousMonster is a simple
extension to Monster that produces a new interface. This is
implemented in DragonZilla.
The syntax used in Vampire
works only when inheriting interfaces. Normally, you can use
extends with only a single class, but since an
interface can be made from multiple other interfaces, extends can
refer to multiple base interfaces when building a new interface. As you
can see, the interface names are simply separated with
commas.
Because any fields you put into an
interface are automatically static and final, the
interface is a convenient tool for
creating groups of constant
values, much as you would with an enum in C or C++. For
example:
//: Months.java // Using interfaces to create groups of constants package c07; public interface Months { int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12; } ///:~
Notice the Java style of using all
uppercase letters (with underscores to separate multiple words in a single
identifier) for static final primitives that have constant
initializers – that is, for compile-time constants.
The fields in an interface
are automatically public, so it’s unnecessary to specify
that.
Now you can use the constants from
outside the package by importing c07.* or c07.Months just as you
would with any other package, and referencing the values with expressions like
Months.JANUARY. Of course, what you get is just an int so there
isn’t the extra type safety that C++’s enum has, but this
(commonly-used) technique is certainly an improvement over hard-coding numbers
into your programs. (This is often referred to as using “magic
numbers” and it produces very difficult-to-maintain
code.)
If you do want extra type safety,
you can build a class like
this:[28]
//: Month2.java // A more robust enumeration system package c07; public final class Month2 { private String name; private Month2(String nm) { name = nm; } public String toString() { return name; } public final static Month2 JAN = new Month2("January"), FEB = new Month2("February"), MAR = new Month2("March"), APR = new Month2("April"), MAY = new Month2("May"), JUN = new Month2("June"), JUL = new Month2("July"), AUG = new Month2("August"), SEP = new Month2("September"), OCT = new Month2("October"), NOV = new Month2("November"), DEC = new Month2("December"); public final static Month2[] month = { JAN, JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }; public static void main(String[] args) { Month2 m = Month2.JAN; System.out.println(m); m = Month2.month[12]; System.out.println(m); System.out.println(m == Month2.DEC); System.out.println(m.equals(Month2.DEC)); } } ///:~
The class is called Month2
since there’s already a Month in the standard Java library.
It’s a final class with a private constructor so no one can
inherit from it or make any instances of it. The only instances are the final
static ones created in the class itself: JAN, FEB, MAR,
etc. These objects are also used in the array month, which lets you
choose months by number instead of by name. (Notice the extra JAN in the
array to provide an offset by one, so that December is month 12.) In
main( ) you can see the type safety: m
is a Month2 object so it can be assigned only to a Month2. The
previous example Months.java provided only int values, so an
int variable intended to represent a month could actually be given any
integer value, which wasn’t too safe.
This approach also allows you to
use == or equals( ) interchangeably, as shown at the end of
main( ).
Fields defined in interfaces are
automatically static and final. These cannot be “blank
finals,” but they can be initialized with non-constant expressions. For
example:
//: RandVals.java // Initializing interface fields with // non-constant initializers import java.util.*; public interface RandVals { int rint = (int)(Math.random() * 10); long rlong = (long)(Math.random() * 10); float rfloat = (float)(Math.random() * 10); double rdouble = Math.random() * 10; } ///:~
Since the fields are static,
they are initialized when the class is first loaded, upon first access of any of
the fields. Here’s a simple test:
//: TestRandVals.java public class TestRandVals { public static void main(String[] args) { System.out.println(RandVals.rint); System.out.println(RandVals.rlong); System.out.println(RandVals.rfloat); System.out.println(RandVals.rdouble); } } ///:~
The fields, of course, are not part
of the interface but instead are stored in the static storage area for
that interface.
In Java 1.1
it’s possible to place a class definition within another class definition.
This is called an inner class. The
inner class is a useful feature
because it allows you to group classes that logically belong together and to
control the visibility of one within the other. However, it’s important to
understand that inner classes are distinctly different from
composition.
Often, while you're learning about
them, the need for inner classes isn’t immediately obvious. At the end of
this section, after all of the syntax and semantics of inner classes have been
described, you’ll find an example that should make clear the benefits of
inner classes.
You create an inner class just as
you’d expect: by placing the class definition inside a surrounding class:
//: Parcel1.java // Creating inner classes package c07.parcel1; public class Parcel1 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } // Using inner classes looks just like // using any other class, within Parcel1: public void ship(String dest) { Contents c = new Contents(); Destination d = new Destination(dest); } public static void main(String[] args) { Parcel1 p = new Parcel1(); p.ship("Tanzania"); } } ///:~
The inner classes, when used inside
ship( ), look just like the use of any other classes. Here, the only
practical difference is that the names are nested within Parcel1.
You’ll see in a while that this isn’t the only
difference.
More typically, an outer class will
have a method that returns a handle to an inner class, like
this:
//: Parcel2.java // Returning a handle to an inner class package c07.parcel2; public class Parcel2 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public Destination to(String s) { return new Destination(s); } public Contents cont() { return new Contents(); } public void ship(String dest) { Contents c = cont(); Destination d = to(dest); } public static void main(String[] args) { Parcel2 p = new Parcel2(); p.ship("Tanzania"); Parcel2 q = new Parcel2(); // Defining handles to inner classes: Parcel2.Contents c = q.cont(); Parcel2.Destination d = q.to("Borneo"); } } ///:~
If you want to make an object of
the inner class anywhere except from within a non-static method of the
outer class, you must specify the type of that object as
OuterClassName.InnerClassName, as seen in
main( ).
So far, inner classes don’t
seem that dramatic. After all, if it’s hiding you’re after, Java
already has a perfectly good hiding mechanism – just allow the class to be
“friendly” (visible only within a
package) rather than creating it
as an inner class.
However,
inner classes really come into their own when you start upcasting to a base
class, and in particular to an interface. (The effect of producing an
interface handle from an object that implements it is essentially the same as
upcasting to a base class.) That’s because the inner class can then be
completely unseen and unavailable to anyone, which is convenient for hiding the
implementation. All you get back is a handle to the base class or the
interface, and it’s possible that you can’t even find out the
exact type, as shown here:
//: Parcel3.java // Returning a handle to an inner class package c07.parcel3; abstract class Contents { abstract public int value(); } interface Destination { String readLabel(); } public class Parcel3 { private class PContents extends Contents { private int i = 11; public int value() { return i; } } protected class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } public Destination dest(String s) { return new PDestination(s); } public Contents cont() { return new PContents(); } } class Test { public static void main(String[] args) { Parcel3 p = new Parcel3(); Contents c = p.cont(); Destination d = p.dest("Tanzania"); // Illegal -- can't access private class: //! Parcel3.PContents c = p.new PContents(); } } ///:~
Now Contents and
Destination represent interfaces available to the client programmer. (The
interface, remember, automatically makes all of its members
public.) For convenience, these are placed inside a single file, but
ordinarily Contents and Destination would each be public in
their own files.
In Parcel3, something new
has been added: the inner class PContents is private so no one but
Parcel3 can access it. PDestination is protected, so no one
but Parcel3, classes in the Parcel3 package (since
protected also gives package access; that is, protected is also
“friendly”), and the inheritors of Parcel3 can access
PDestination. This means that the client programmer has restricted
knowledge and access to these members. In fact, you can’t even downcast to
a private inner class (or a protected inner class unless
you’re an inheritor), because you can’t access the name, as you can
see in class Test. Thus, the private inner class provides a way
for the class designer to completely prevent any type-coding dependencies and to
completely hide details about implementation. In addition, extension of an
interface is useless from the client programmer’s perspective since
the client programmer cannot access any additional methods that aren’t
part of the public interface class. This also provides an opportunity for
the Java compiler to generate more efficient code.
Normal (non-inner) classes cannot
be made private or protected – only public or
“friendly.”
Note that Contents
doesn’t need to be an abstract class. You could use an ordinary
class here as well, but the most typical starting point for such a design is an
interface.
What you’ve seen so far
encompasses the typical use for inner classes. In general, the code that
you’ll write and read involving inner classes will be “plain”
inner classes that are simple and easy to understand. However, the design for
inner classes is quite complete and there are a number of other, more obscure,
ways that you can use them if you choose: inner classes can be created within a
method or even an arbitrary scope. There are two reasons for doing
this:
This will
all take place within the package innerscopes. First, the common
interfaces from the previous code will be defined in their own files so they can
be used in all the examples:
//: Destination.java package c07.innerscopes; interface Destination { String readLabel(); } ///:~
The point has been made that
Contents could be an abstract class, so here it will be in a more
natural form, as an interface:
//: Contents.java package c07.innerscopes; interface Contents { int value(); } ///:~
Although it’s an ordinary
class with an implementation, Wrapping is also being used as a common
“interface” to its derived classes:
//: Wrapping.java package c07.innerscopes; public class Wrapping { private int i; public Wrapping(int x) { i = x; } public int value() { return i; } } ///:~
You’ll notice above that
Wrapping has a constructor that requires an argument, to make things a
bit more interesting.
The first example shows the
creation of an entire class within the scope of a method (instead of the scope
of another class):
//: Parcel4.java // Nesting a class within a method package c07.innerscopes; public class Parcel4 { public Destination dest(String s) { class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } return new PDestination(s); } public static void main(String[] args) { Parcel4 p = new Parcel4(); Destination d = p.dest("Tanzania"); } } ///:~
The class PDestination is
part of dest( ) rather than being part of Parcel4. (Also
notice that you could use the class identifier PDestination for an inner
class inside each class in the same subdirectory without a name clash.)
Therefore, PDestination cannot be accessed outside of
dest( ). Notice the upcasting that occurs in the return
statement – nothing comes out of dest( ) except a handle to
the base class Destination. Of course, the fact that the name of the
class PDestination is placed inside dest( ) doesn’t
mean that PDestination is not a valid object once dest( )
returns.
//: Parcel5.java // Nesting a class within a scope package c07.innerscopes; public class Parcel5 { private void internalTracking(boolean b) { if(b) { class TrackingSlip { private String id; TrackingSlip(String s) { id = s; } String getSlip() { return id; } } TrackingSlip ts = new TrackingSlip("slip"); String s = ts.getSlip(); } // Can't use it here! Out of scope: //! TrackingSlip ts = new TrackingSlip("x"); } public void track() { internalTracking(true); } public static void main(String[] args) { Parcel5 p = new Parcel5(); p.track(); } } ///:~
The class TrackingSlip is
nested inside the scope of an if statement. This does not mean that the
class is conditionally created – it gets compiled along with everything
else. However, it’s not available outside the scope in which it is
defined. Other than that, it looks just like an ordinary
class.
The next example looks a little
strange:
//: Parcel6.java // A method that returns an anonymous inner class package c07.innerscopes; public class Parcel6 { public Contents cont() { return new Contents() { private int i = 11; public int value() { return i; } }; // Semicolon required in this case } public static void main(String[] args) { Parcel6 p = new Parcel6(); Contents c = p.cont(); } } ///:~
The cont( ) method
combines the creation of the return value with the definition of the class that
represents that return value! In addition, the class is anonymous – it has
no name. To make matters a bit worse, it looks like you’re starting out to
create a Contents object:
return new Contents()
but then, before you get to the
semicolon, you say, “But wait, I think I’ll slip in a class
definition”:
return new Contents() { private int i = 11; public int value() { return i; } };
What this strange syntax means is
“create an object of an anonymous class that’s inherited from
Contents.” The handle returned by the new expression is
automatically upcast to a Contents handle. The anonymous inner class
syntax is a shorthand for:
class MyContents extends Contents { private int i = 11; public int value() { return i; } } return new MyContents();
In the anonymous inner class,
Contents is created using a default constructor. The following code shows
what to do if your base class needs a constructor with an
argument:
//: Parcel7.java // An anonymous inner class that calls the // base-class constructor package c07.innerscopes; public class Parcel7 { public Wrapping wrap(int x) { // Base constructor call: return new Wrapping(x) { public int value() { return super.value() * 47; } }; // Semicolon required } public static void main(String[] args) { Parcel7 p = new Parcel7(); Wrapping w = p.wrap(10); } } ///:~
That is, you simply pass the
appropriate argument to the base-class constructor, seen here as the x
passed in new Wrapping(x). An anonymous class cannot have a
constructor where you would normally call super( ).
In both of the previous examples,
the semicolon doesn’t mark the end of the class body (as it does in C++).
Instead, it marks the end of the expression that happens to contain the
anonymous class. Thus, it’s identical to the use of the semicolon
everywhere else.
What happens if you need to perform
some kind of initialization for an object of an
anonymous
inner class? Since it’s anonymous, there’s no name to give the
constructor so you can’t have a constructor. You can, however, perform
initialization at the point of definition of your fields:
//: Parcel8.java // An anonymous inner class that performs // initialization. A briefer version // of Parcel5.java. package c07.innerscopes; public class Parcel8 { // Argument must be final to use inside // anonymous inner class: public Destination dest(final String dest) { return new Destination() { private String label = dest; public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel8 p = new Parcel8(); Destination d = p.dest("Tanzania"); } } ///:~
If you’re defining an
anonymous inner class and want to use an object that’s defined outside the
anonymous inner class, the compiler requires that the outside object be
final. This is why the argument to dest( ) is
final. If you forget, you’ll get a compile-time error
message.
As long as you’re simply
assigning a field, the above approach is fine. But what if you need to perform
some constructor-like activity? With Java 1.1
instance initialization,
you can, in effect, create a constructor for an anonymous inner
class:
//: Parcel9.java // Using "instance initialization" to perform // construction on an anonymous inner class package c07.innerscopes; public class Parcel9 { public Destination dest(final String dest, final float price) { return new Destination() { private int cost; // Instance initialization for each object: { cost = Math.round(price); if(cost > 100) System.out.println("Over budget!"); } private String label = dest; public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel9 p = new Parcel9(); Destination d = p.dest("Tanzania", 101.395F); } } ///:~
Inside the instance initializer you
can see code that couldn’t be executed as part of a field initializer
(that is, the if statement). So in effect, an instance initializer is the
constructor for an anonymous inner class. Of course, it’s limited; you
can’t overload instance initializers so you can have only one of these
constructors.
So far, it appears that inner
classes are just a name-hiding and code-organization scheme, which is helpful
but not totally compelling. However, there’s another twist. When you
create an inner class, objects of that inner class have a link to the enclosing
object that made them, and so they can access the members of that enclosing
object – without any special qualifications. In addition,
inner
classes have access rights to all the elements in the enclosing
class.[29]
The following example demonstrates this:
//: Sequence.java // Holds a sequence of Objects interface Selector { boolean end(); Object current(); void next(); } public class Sequence { private Object[] o; private int next = 0; public Sequence(int size) { o = new Object[size]; } public void add(Object x) { if(next < o.length) { o[next] = x; next++; } } private class SSelector implements Selector { int i = 0; public boolean end() { return i == o.length; } public Object current() { return o[i]; } public void next() { if(i < o.length) i++; } } public Selector getSelector() { return new SSelector(); } public static void main(String[] args) { Sequence s = new Sequence(10); for(int i = 0; i < 10; i++) s.add(Integer.toString(i)); Selector sl = s.getSelector(); while(!sl.end()) { System.out.println((String)sl.current()); sl.next(); } } } ///:~
The Sequence is simply a
fixed-sized array of Object with a class wrapped around it. You call
add( ) to add a new Object to the end of the sequence (if
there’s room left). To fetch each of the objects in a Sequence,
there’s an interface called Selector, which allows you to see if
you’re at the end( ), to look at the current( )
Object, and to move to the next( ) Object in the
Sequence. Because Selector is an interface, many other
classes can implement the interface in their own ways, and many methods
can take the interface as an argument, in order to create generic
code.
Here, the SSelector is a
private class that provides Selector functionality. In
main( ), you can see the creation of a Sequence, followed by
the addition of a number of String objects. Then, a Selector is
produced with a call to getSelector( ) and this is used to move
through the Sequence and select each item.
At first, the creation of
SSelector looks like just another inner class. But examine it closely.
Note that each of the methods end( ), current( ), and
next( ) refer to o, which is a handle that isn’t part
of SSelector, but is instead a private field in the enclosing
class. However, the inner class can access methods and fields from the enclosing
class as if they owned them. This turns out to be very convenient, as you can
see in the above example.
So an inner class has access to the
members of the enclosing class. How can this happen? The
inner class must keep a reference to the particular
object of the enclosing class that was responsible for creating it. Then when
you refer to a member of the enclosing class, that (hidden) reference is used to
select that member. Fortunately, the compiler takes care of all these details
for you, but you can also understand now that an object of an inner class can be
created only in association with an object of the enclosing class. The process
of construction requires the initialization of the handle to the object of the
enclosing class, and the compiler will complain if it cannot access the handle.
Most of the time this occurs without any intervention on the part of the
programmer.
To understand the meaning of
static when applied to
inner classes, you must remember that the object of the inner class implicitly
keeps a handle to the object of the enclosing class that created it. This is not
true, however, when you say an inner class is static. A static
inner class means:
There are some
restrictions: static members can be at only the outer level of a class,
so inner classes cannot have static data or static inner
classes.
If
you don’t need to create an object of the outer class in order to create
an object of the inner class, you can make everything static. For this to
work, you must also make the inner classes static:
//: Parcel10.java // Static inner classes package c07.parcel10; abstract class Contents { abstract public int value(); } interface Destination { String readLabel(); } public class Parcel10 { private static class PContents extends Contents { private int i = 11; public int value() { return i; } } protected static class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } public static Destination dest(String s) { return new PDestination(s); } public static Contents cont() { return new PContents(); } public static void main(String[] args) { Contents c = cont(); Destination d = dest("Tanzania"); } } ///:~
In main( ), no object
of Parcel10 is necessary; instead you use the normal syntax for selecting
a static member to call the methods that return handles to
Contents and Destination.
Normally you can't put any code
inside an interface, but a static inner class can be part of an
interface. Since the class is static it doesn't violate the rules
for interfaces – the static inner class is only placed inside the
namespace of the interface:
//: IInterface.java // Static inner classes inside interfaces interface IInterface { static class Inner { int i, j, k; public Inner() {} void f() {} } } ///:~
Earlier in the book I suggested
putting a main( ) in every class to act as a
test bed for that class. One drawback to this is the
amount of extra code you must carry around. If this is a problem, you can use a
static inner class to hold your test code:
//: TestBed.java // Putting test code in a static inner class class TestBed { TestBed() {} void f() { System.out.println("f()"); } public static class Tester { public static void main(String[] args) { TestBed t = new TestBed(); t.f(); } } } ///:~
This generates a separate class
called TestBed$Tester (to run the program you say java
TestBed$Tester). You can use this class for testing, but you don't need to
include it in your shipping
product.
If you need to produce the handle
to the outer class object, you name the outer class followed by a dot and
this. For example, in the class Sequence.SSelector, any of its
methods can produce the stored handle to the outer class Sequence by
saying Sequence.this. The resulting handle is automatically the correct
type. (This is known and checked at compile time, so there is no run-time
overhead.)
Sometimes you want to tell some
other object to create an object of one of its inner classes. To do this you
must provide a handle to the other outer class object in the new
expression, like this:
//: Parcel11.java // Creating inner classes package c07.parcel11; public class Parcel11 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public static void main(String[] args) { Parcel11 p = new Parcel11(); // Must use instance of outer class // to create an instances of the inner class: Parcel11.Contents c = p.new Contents(); Parcel11.Destination d = p.new Destination("Tanzania"); } } ///:~
To create an object of the inner
class directly, you don’t follow the same form and refer to the outer
class name Parcel11 as you might expect, but instead you must use an
object of the outer class to make an object of the inner
class:
Parcel11.Contents c = p.new Contents();
Thus, it’s not possible to
create an object of the inner class unless you already have an object of the
outer class. This is because the object of the inner class is quietly connected
to the object of the outer class that it was made from. However, if you make a
static inner class, then it doesn’t need a handle to the outer
class object.
Because the inner class constructor
must attach to a handle of the enclosing class object, things are slightly
complicated when you inherit from an inner class. The problem is that the
“secret” handle to the enclosing class object must be
initialized, and yet in the derived class there’s no longer a default
object to attach to. The answer is to use a syntax provided to make the
association explicit:
//: InheritInner.java // Inheriting an inner class class WithInner { class Inner {} } public class InheritInner extends WithInner.Inner { //! InheritInner() {} // Won't compile InheritInner(WithInner wi) { wi.super(); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } } ///:~
You can see that
InheritInner is extending only the inner class, not the outer one. But
when it comes time to create a constructor, the default one is no good and you
can’t just pass a handle to an enclosing object. In addition, you must use
the syntax
enclosingClassHandle.super();
What happens when you create an
inner class, then inherit from the enclosing class and redefine the inner class?
That is, is it possible to override an inner class? This seems like it would be
a powerful concept, but
“overriding”
an inner class as if it were another method of the outer class doesn’t
really do anything:
//: BigEgg.java // An inner class cannot be overriden // like a method class Egg { protected class Yolk { public Yolk() { System.out.println("Egg.Yolk()"); } } private Yolk y; public Egg() { System.out.println("New Egg()"); y = new Yolk(); } } public class BigEgg extends Egg { public class Yolk { public Yolk() { System.out.println("BigEgg.Yolk()"); } } public static void main(String[] args) { new BigEgg(); } } ///:~
The default constructor is
synthesized automatically by the compiler, and this calls the base-class default
constructor. You might think that since a BigEgg is being created, the
“overridden” version of Yolk would be used, but this is not
the case. The output is:
New Egg() Egg.Yolk()
This example simply shows that
there isn’t any extra inner class magic going on when you inherit from the
outer class. However, it’s still possible to explicitly inherit from the
inner class:
//: BigEgg2.java // Proper inheritance of an inner class class Egg2 { protected class Yolk { public Yolk() { System.out.println("Egg2.Yolk()"); } public void f() { System.out.println("Egg2.Yolk.f()"); } } private Yolk y = new Yolk(); public Egg2() { System.out.println("New Egg2()"); } public void insertYolk(Yolk yy) { y = yy; } public void g() { y.f(); } } public class BigEgg2 extends Egg2 { public class Yolk extends Egg2.Yolk { public Yolk() { System.out.println("BigEgg2.Yolk()"); } public void f() { System.out.println("BigEgg2.Yolk.f()"); } } public BigEgg2() { insertYolk(new Yolk()); } public static void main(String[] args) { Egg2 e2 = new BigEgg2(); e2.g(); } } ///:~
Now BiggEgg2.Yolk explicitly
extends Egg2.Yolk and overrides its methods. The method
insertYolk( ) allows BigEgg2 to upcast one of its own Yolk
objects into the y handle in Egg2, so when g( )
calls y.f( ) the overridden version of f( ) is used. The
output is:
Egg2.Yolk() New Egg2() Egg2.Yolk() BigEgg2.Yolk() BigEgg2.Yolk.f()
The second call to
Egg2.Yolk( ) is the base-class constructor call of the
BigEgg2.Yolk constructor. You can see that the overridden version of
f( ) is used when g( ) is
called.
Since every class produces a
.class file that holds all the information about how to create objects of
this type (this information produces a meta-class called the Class
object), you might guess that
inner classes must also produce
.class files to contain the information for their Class
objects. The names of these files/classes have a strict formula: the name of the
enclosing class, followed by a ‘$’, followed by the name of
the inner class. For example, the .class files created by
InheritInner.java include:
InheritInner.class WithInner$Inner.class WithInner.class
If inner classes are anonymous, the
compiler simply starts generating numbers as inner class identifiers. If inner
classes are nested within inner classes, their names are simply appended after a
‘$’ and the outer class identifier(s).
Although this scheme of generating
internal names is simple and straightforward, it’s also robust and handles
most
situations.[30]
Since it is the standard naming scheme for Java, the generated files are
automatically platform-independent. (Note that the Java compiler is changing
your inner classes in all sorts of other ways in order to make them
work.)
At this point you’ve seen a
lot of syntax and semantics describing the way inner classes work, but this
doesn’t answer the question of why they exist. Why did Sun go to so much
trouble to add such a fundamental language feature in Java
1.1? The answer is something that I will refer to here as
a
control
framework.
An
application framework is a class or a set of
classes that’s designed to solve a particular type of problem. To apply an
application framework, you inherit from one or more classes and override some of
the methods. The code you write in the overridden methods customizes the general
solution provided by that application framework to solve your specific problem.
The control framework is a particular type of application framework
dominated by the need to respond to events; a system that primarily responds to
events is called an event-driven system.
One of the most important problems in application programming is the
graphical
user interface (GUI), which is almost entirely event-driven. As you will see in
Chapter 13, the Java 1.1 AWT is a control framework that elegantly solves the
GUI problem using inner classes.
To see how inner classes allow the
simple creation and use of control frameworks, consider a control framework
whose job is to execute events whenever those events are “ready.”
Although “ready” could mean anything, in this case the default will
be based on clock time. What follows is a control framework that contains no
specific information about what it’s controlling. First, here is the
interface that describes any control event. It’s an abstract class
instead of an actual interface because the default behavior is control
based on time, so some of the implementation can be included
here:
//: Event.java // The common methods for any control event package c07.controller; abstract public class Event { private long evtTime; public Event(long eventTime) { evtTime = eventTime; } public boolean ready() { return System.currentTimeMillis() >= evtTime; } abstract public void action(); abstract public String description(); } ///:~
The constructor simply captures the
time when you want the Event to run, while ready( ) tells you
when it’s time to run it. Of course, ready( ) could be
overridden in a derived class to base the Event on something other than
time.
action( ) is the method
that’s called when the Event is ready( ), and
description( ) gives textual information about the
Event.
The next file contains the actual
control framework that manages and fires events. The first class is really just
a “helper” class whose job is to hold Event objects. You
could replace it with any appropriate collection, and in Chapter 8 you’ll
discover other collections that will do the trick without requiring you to write
this extra code:
//: Controller.java // Along with Event, the generic // framework for all control systems: package c07.controller; // This is just a way to hold Event objects. class EventSet { private Event[] events = new Event[100]; private int index = 0; private int next = 0; public void add(Event e) { if(index >= events.length) return; // (In real life, throw exception) events[index++] = e; } public Event getNext() { boolean looped = false; int start = next; do { next = (next + 1) % events.length; // See if it has looped to the beginning: if(start == next) looped = true; // If it loops past start, the list // is empty: if((next == (start + 1) % events.length) && looped) return null; } while(events[next] == null); return events[next]; } public void removeCurrent() { events[next] = null; } } public class Controller { private EventSet es = new EventSet(); public void addEvent(Event c) { es.add(c); } public void run() { Event e; while((e = es.getNext()) != null) { if(e.ready()) { e.action(); System.out.println(e.description()); es.removeCurrent(); } } } } ///:~
EventSet arbitrarily holds
100 Events. (If a “real” collection from Chapter 8 is used
here you don’t need to worry about its maximum size, since it will resize
itself). The index is used to keep track of the next available space, and
next is used when you’re looking for the next Event in the
list, to see whether you’ve looped around. This is important during a call
to getNext( ), because Event objects are removed from the
list (using removeCurrent( )) once they’re run, so
getNext( ) will encounter holes in the list as it moves through
it.
Note that
removeCurrent( ) doesn’t just set some flag indicating that
the object is no longer in use. Instead, it sets the handle to null. This
is important because if the garbage collector sees a
handle that’s still in use then it can’t clean up the object. If you
think your handles might hang around (as they would here), then it’s a
good idea to set them to null to give the garbage collector permission to
clean them up.
Controller is where the
actual work goes on. It uses an EventSet to hold its Event
objects, and addEvent( ) allows you to add new events to this list.
But the important method is run( ). This method loops through the
EventSet, hunting for an Event object that’s
ready( ) to run. For each one it finds ready( ),
it calls the action( ) method, prints out the
description( ), and then removes the Event from the list.
Note that so far in this design you
know nothing about exactly what an Event does. And this is the
crux of the design; how it “separates the things that change from the
things that stay the same.” Or, to use my term, the
“vector of change” is
the different actions of the various kinds of Event objects, and you
express different actions by creating different Event
subclasses.
Consider a
particular implementation of the control framework designed to control
greenhouse
functions.[31]
Each action is entirely different: turning lights, water, and thermostats on and
off, ringing bells, and restarting the system. But the control framework is
designed to easily isolate this different code. For each type of action you
inherit a new Event inner class, and write the control code inside of
action( ).
As is typical with an application
framework, the class GreenhouseControls is inherited from
Controller:
//: GreenhouseControls.java // This produces a specific application of the // control system, all in a single class. Inner // classes allow you to encapsulate different // functionality for each type of event. package c07.controller; public class GreenhouseControls extends Controller { private boolean light = false; private boolean water = false; private String thermostat = "Day"; private class LightOn extends Event { public LightOn(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here to // physically turn on the light. light = true; } public String description() { return "Light is on"; } } private class LightOff extends Event { public LightOff(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here to // physically turn off the light. light = false; } public String description() { return "Light is off"; } } private class WaterOn extends Event { public WaterOn(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here water = true; } public String description() { return "Greenhouse water is on"; } } private class WaterOff extends Event { public WaterOff(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here water = false; } public String description() { return "Greenhouse water is off"; } } private class ThermostatNight extends Event { public ThermostatNight(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here thermostat = "Night"; } public String description() { return "Thermostat on night setting"; } } private class ThermostatDay extends Event { public ThermostatDay(long eventTime) { super(eventTime); } public void action() { // Put hardware control code here thermostat = "Day"; } public String description() { return "Thermostat on day setting"; } } // An example of an action() that inserts a // new one of itself into the event list: private int rings; private class Bell extends Event { public Bell(long eventTime) { super(eventTime); } public void action() { // Ring bell every 2 seconds, rings times: System.out.println("Bing!"); if(--rings > 0) addEvent(new Bell( System.currentTimeMillis() + 2000)); } public String description() { return "Ring bell"; } } private class Restart extends Event { public Restart(long eventTime) { super(eventTime); } public void action() { long tm = System.currentTimeMillis(); // Instead of hard-wiring, you could parse // configuration information from a text // file here: rings = 5; addEvent(new ThermostatNight(tm)); addEvent(new LightOn(tm + 1000)); addEvent(new LightOff(tm + 2000)); addEvent(new WaterOn(tm + 3000)); addEvent(new WaterOff(tm + 8000)); addEvent(new Bell(tm + 9000)); addEvent(new ThermostatDay(tm + 10000)); // Can even add a Restart object! addEvent(new Restart(tm + 20000)); } public String description() { return "Restarting system"; } } public static void main(String[] args) { GreenhouseControls gc = new GreenhouseControls(); long tm = System.currentTimeMillis(); gc.addEvent(gc.new Restart(tm)); gc.run(); } } ///:~
Note that light,
water, thermostat, and rings all belong to the outer class
GreenhouseControls, and yet the inner classes have no problem accessing
those fields. Also, most of the action( ) methods also involve some
sort of hardware control, which would most likely involve calls to non-Java
code.
Most of the Event classes
look similar, but Bell and Restart are special. Bell rings,
and if it hasn’t yet rung enough times it adds a new Bell object to
the event list, so it will ring again later. Notice how inner classes
almost look like multiple inheritance: Bell has all the methods of
Event and it also appears to have all the methods of the outer class
GreenhouseControls.
Restart is responsible for
initializing the system, so it adds all the appropriate events. Of course, a
more flexible way to accomplish this is to avoid hard-coding the events and
instead read them from a file. (An exercise in Chapter 10 asks you to modify
this example to do just that.) Since Restart( ) is just another
Event object, you can also add a Restart object within
Restart.action( ) so that the system regularly restarts itself. And
all you need to do in main( ) is create a GreenhouseControls
object and add a Restart object to get it going.
This example should move you a long
way toward appreciating the value of inner classes, especially when used within
a control framework. However, in the latter half of Chapter 13 you’ll see
how elegantly inner classes are used to describe the actions of a graphical user
interface. By the time you finish that section you should be fully
convinced.
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 11), 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, but that was before inheritance and polymorphism
were introduced.
A constructor for the base class is
always called in the constructor for a derived class, chaining upward 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 properly. 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:
//: 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:
//: 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!"); // Must do this to guarantee that all // finalizers will be called: System.runFinalizersOnExit(true); } } ///:~
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,
System.runFinalizersOnExit(true) adds the extra
overhead to guarantee that finalization takes place. 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. 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 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 “forward” or
“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:
//: 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 zero. This would probably result in
either a dot or nothing at all being drawn on the screen, and you’d be
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 handles that are embedded inside a class via composition.
So if you forget to initialize that handle 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:
//: Transmogrify.java // Dynamically changing the behavior of // an object via composition. interface Actor { void act(); } class HappyActor implements Actor { public void act() { System.out.println("HappyActor"); } } class SadActor implements 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
handle to an Actor, which is initialized to a HappyActor object.
This means go( ) produces a particular behavior. But since a handle
can be re-bound to a different object at run time, a handle 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. 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 member variables 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 promote) 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 sections show 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:
//: 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 11 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, 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.
[28]
This approach was inspired by an e-mail from Rich Hoffarth.
[29]
This is very different from the design of nested classes in C++, which is
simply a name-hiding mechanism. There is no link to an enclosing object and no
implied permissions in C++.
[30]
On the other hand, ‘$’ is a meta-character to the Unix shell and so
you’ll sometimes have trouble when listing the .class files. This
is a bit strange coming from Sun, a Unix-based company. My guess is that they
weren’t considering this issue, but instead thought you’d naturally
focus on the source-code files.
[31]
For some reason this has always been a pleasing problem for me to solve; it came
from C++ Inside & Out, but Java allows a much more elegant
solution.