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: (See page 141 if you have trouble executing this
program.)
//: c07:Music.java // Inheritance & upcasting 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:
//: c07: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:
//: c07:Shapes.java // Polymorphism in Java class Shape { void draw() {} void erase() {} } class Circle extends Shape { void draw() { System.out.println("Circle.draw()"); } void erase() { System.out.println("Circle.erase()"); } } class Square extends Shape { void draw() { System.out.println("Square.draw()"); } void erase() { System.out.println("Square.erase()"); } } class Triangle extends Shape { void draw() { System.out.println("Triangle.draw()"); } void erase() { System.out.println("Triangle.erase()"); } } public class Shapes { public static Shape randShape() { switch((int)(Math.random() * 3)) { default: // 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:
//: c07: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:
//: c07:WindError.java // Accidentally changing the interface class NoteX { public static final int MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2; } class InstrumentX { public void play(int NoteX) { System.out.println("InstrumentX.play()"); } } class WindX extends InstrumentX { // OOPS! Changes the method interface: public void play(NoteX n) { System.out.println("WindX.play(NoteX n)"); } } public class WindError { public static void tune(InstrumentX i) { // ... i.play(NoteX.MIDDLE_C); } public static void main(String[] args) { WindX flute = new WindX(); tune(flute); // Not the desired behavior! } } ///:~
There’s another confusing aspect
thrown in here. In InstrumentX, the play( ) method takes an
int that has the identifier NoteX. That is, even though
NoteX is a class name, it can also be used as an identifier without
complaint. But in WindX, play( ) takes a NoteX 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:
//: c07: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, 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:
//: c07: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:
//: c07: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:
//: c07: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:
//: c07:Months.java // Using interfaces to create groups of constants 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:[37]
//: c07:Month2.java // A more robust enumeration system 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:
//: c07: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:
//: c07: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:
(See page 141 if you have trouble executing this program.)
//: c07:parcel1: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:
//: c07:parcel2: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:
//: c07:parcel3: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:
In the following
examples, the previous code will be modified to use:
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:
//: c07:innerscopes: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:
//: c07:innerscopes: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:
//: c07:innerscopes: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):
//: c07:innerscopes: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.
//: c07:innerscopes: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:
//: c07:innerscopes: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:
//: c07:innerscopes: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:
//: c07:innerscopes: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:
//: c07:innerscopes: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.[38] The
following example demonstrates this:
//: c07: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(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:
//: c07:parcel10: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:
//: c07: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:
//: c07: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:
//: c07:parcel11: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:
//: c07: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:
//: c07: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:
//: c07: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 BigEgg2.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.[39]
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 Swing library 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:
//: c07:controller: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:
//: c07:controller: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.
This is where inner classes come into
play. They allow two things:
Consider a
particular implementation of the control framework designed to control
greenhouse
functions.[40] 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:
//: c07: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:
//: c07:Sandwich.java // Order of constructor calls class Meal { Meal() { System.out.println("Meal()"); } } class Bread { Bread() { System.out.println("Bread()"); } } class Cheese { Cheese() { System.out.println("Cheese()"); } } class Lettuce { Lettuce() { System.out.println("Lettuce()"); } } class Lunch extends Meal { Lunch() { System.out.println("Lunch()");} } class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } } class Sandwich extends PortableLunch { Bread b = new Bread(); Cheese c = new Cheese(); Lettuce l = new Lettuce(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ///:~
This example creates a complex class out
of other classes, and each class has a constructor that announces itself. The
important class is Sandwich, which reflects three levels of inheritance
(four, if you count the implicit inheritance from Object) and three
member objects. When a Sandwich object is created in main( ),
the output is:
Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich()
This means that the order of constructor
calls for a complex object is as follows:
The
order of the constructor calls is important. When you inherit, you know all
about the base class and can access any public and protected
members of the base class. This means that you must be able to assume that all
the members of the base class are valid when you’re in the derived class.
In a normal method, construction has already taken place, so all the members of
all parts of the object have been built. Inside the constructor, however, you
must be able to assume that all members that you use have been built. The only
way to guarantee this is for the base-class constructor to be called first. Then
when you’re in the derived-class constructor, all the members you can
access in the base class have been initialized. “Knowing that all members
are valid” inside the constructor is also the reason that, whenever
possible, you should initialize all member objects (that is, objects placed in
the class using composition) at their point of definition in the class (e.g.:
b, c, and l in the example above). If you follow this
practice, you will help ensure that all base class members and member
objects of the current object have been initialized. Unfortunately, this
doesn’t handle every case, as you will see in the next
section.
When you use composition to create a new
class, you never worry about finalizing the member objects of that class. Each
member is an independent object and thus is garbage
collected and finalized regardless of whether it happens to be a member of your
class. With inheritance, however, you must override
finalize( ) in the
derived class if you have any special cleanup that must happen as part of
garbage collection. When you override finalize( ) in an inherited
class, it’s important to remember to call the base-class version of
finalize( ), since otherwise the base-class finalization will not
happen. The following example proves this:
//: c07:Frog.java // Testing finalize with inheritance class DoBaseFinalization { public static boolean flag = false; } class Characteristic { String s; Characteristic(String c) { s = c; System.out.println( "Creating Characteristic " + s); } protected void finalize() { System.out.println( "finalizing Characteristic " + s); } } class LivingCreature { Characteristic p = new Characteristic("is alive"); LivingCreature() { System.out.println("LivingCreature()"); } protected void finalize() { System.out.println( "LivingCreature finalize"); // Call base-class version LAST! if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } class Animal extends LivingCreature { Characteristic p = new Characteristic("has heart"); Animal() { System.out.println("Animal()"); } protected void finalize() { System.out.println("Animal finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } class Amphibian extends Animal { Characteristic p = new Characteristic("can live in water"); Amphibian() { System.out.println("Amphibian()"); } protected void finalize() { System.out.println("Amphibian finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } } public class Frog extends Amphibian { Frog() { System.out.println("Frog()"); } protected void finalize() { System.out.println("Frog finalize"); if(DoBaseFinalization.flag) try { super.finalize(); } catch(Throwable t) {} } public static void main(String[] args) { if(args.length != 0 && args[0].equals("finalize")) DoBaseFinalization.flag = true; else System.out.println("not finalizing bases"); new Frog(); // Instantly becomes garbage System.out.println("bye!"); // 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:
//: c07:PolyConstructors.java // Constructors and polymorphism // don't produce what you might expect. abstract class Glyph { abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { int radius = 1; RoundGlyph(int r) { radius = r; System.out.println( "RoundGlyph.RoundGlyph(), radius = " + radius); } void draw() { System.out.println( "RoundGlyph.draw(), radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } ///:~
In Glyph, the draw( )
method is abstract, so it is designed to be overridden. Indeed, you are
forced to override it in RoundGlyph. But the Glyph constructor
calls this method, and the call ends up in RoundGlyph.draw( ), which
would seem to be the intent. But look at the output:
Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5
When Glyph’s constructor
calls draw( ), the value of radius isn’t even the
default initial value 1. It’s 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:
//: c07: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:
//: c07:RTTI.java // Downcasting & Run-Time Type // Identification (RTTI) import java.util.*; class Useful { public void f() {} public void g() {} } class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {} } public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Compile-time: method not found in Useful: //! x[1].u(); ((MoreUseful)x[1]).u(); // Downcast/RTTI ((MoreUseful)x[0]).u(); // Exception thrown } } ///:~
As in the diagram, MoreUseful
extends the interface of Useful. But since it’s inherited, it can
also be upcast to a Useful. You can see this happening in the
initialization of the array x in main( ). Since both objects
in the array are of class Useful, you can send the f( ) and
g( ) methods to both, and if you try to call u( ) (which
exists only in MoreUseful) you’ll get a compile-time error
message.
If you want to access the extended
interface of a MoreUseful object, you can try to downcast. If it’s
the correct type, it will be successful. Otherwise, you’ll get a
ClassCastException. You don’t need to write
any special code for this exception, since it indicates a programmer error that
could happen anywhere in a program.
There’s more to RTTI than a simple
cast. For example, there’s a way to see what type you’re dealing
with before you try to downcast it. All of Chapter 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.
[37]
This approach was inspired by an e-mail from Rich Hoffarth.
[38]
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++.
[39]
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.
[40]
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.