|
|||||
}
class Wind extends
Instrument {
public void play()
{
System.out.println("Wind.play()");
}
public
String what() { return "Wind"; }
public
void adjust() {}
}
class
Percussion extends Instrument {
public
void play() {
System.out.println("Percussion.play()");
}
public
String what() { return "Percussion"; }
public
void adjust() {}
}
class
Stringed extends Instrument {
public
void play() {
System.out.println("Stringed.play()");
}
public
String what() { return "Stringed"; }
public
void adjust() {}
}
class
Brass extends Wind {
public
void play() {
System.out.println("Brass.play()");
}
public
void adjust() {
System.out.println("Brass.adjust()");
}
}
class
Woodwind extends Wind {
public
void play() {
System.out.println("Woodwind.play()");
}
public
String what() { return "Woodwind"; }
}
322
Thinking
in Java
public
class Music3 {
//
Doesn't care about type, so new types
//
added to the system still work right:
static
void tune(Instrument i) {
//
...
i.play();
}
static
void tuneAll(Instrument[] e) {
for(int
i = 0; i < e.length; i++)
tune(e[i]);
}
public
static void main(String[] args) {
Instrument[]
orchestra = new Instrument[5];
int
i = 0;
//
Upcasting during addition to the array:
orchestra[i++]
= new Wind();
orchestra[i++]
= new Percussion();
orchestra[i++]
= new Stringed();
orchestra[i++]
= new Brass();
orchestra[i++]
= new Woodwind();
tuneAll(orchestra);
}
}
///:~
The
new methods are what(
),
which returns a String
reference
with a
description
of the class, and adjust(
),
which provides some way to
adjust
each
instrument.
In
main(
),
when you place something
inside the Instrument
array
you
automatically
upcast to Instrument.
You
can see that the
tune( )
method
is blissfully ignorant of all
the code
changes
that have happened around
it, and yet it works
correctly. This is
exactly
what polymorphism is supposed to
provide. Your code
changes
don't
cause damage to parts of the
program that should not be
affected.
Put
another way, polymorphism is
one of the most important
techniques
that
allow the programmer to
"separate the things that
change from the
things
that stay the
same."
Chapter
7: Polymorphism
323
Overriding
vs. overloading
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!
}
}
///:~
324
Thinking
in Java
There's
another confusing aspect
thrown in here. In InstrumentX,
the
play(
) method
takes an int
that
has the identifier NoteX.
That is, even
though
NoteX is
a class name, it can also be
used as an identifier
without
complaint.
But in WindX,
play( )
takes
a NoteX
reference
that has an
identifier
n. (Although
you could even say
play(NoteX NoteX)
without
an
error.) Thus it appears that
the programmer intended to
override
play(
) but
mistyped the method a bit.
The compiler, however,
assumed
that
an overload and not an
override was intended. Note
that if you follow
the
standard Java naming
convention, the argument
identifier would be
noteX
(lowercase
`n'), which would
distinguish it from the
class name.
In
tune,
the InstrumentX
i is
sent the play(
) message,
with one of
NoteX's
members (MIDDLE_C)
as an argument. Since NoteX
contains
int
definitions,
this means that the
int version
of the now-overloaded
play(
) method
is called, and since that
has not
been
overridden the base-
class
version is used.
The
output is:
InstrumentX.play()
This
certainly doesn't appear to be a
polymorphic method call.
Once you
understand
what's happening, you can
fix the problem fairly
easily, but
imagine
how difficult it might be to
find the bug if it's
buried in a program
of
significant size.
Abstract
classes
and
methods
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.
Chapter
7: Polymorphism
325
Another
way of saying this is to
call Instrument
an
abstract
base class
(or
simply an abstract
class). You
create an abstract class
when you want
to
manipulate a set of classes
through this common
interface. All
derived-
class
methods that match the
signature of the base-class
declaration will
be
called using the dynamic
binding mechanism. (However, as
seen in the
last
section, if the method's
name is the same as the
base class but
the
arguments
are different, you've got
overloading, which probably
isn't what
you
want.)
If
you have an abstract class
like Instrument,
objects of that class
almost
always
have no meaning. That is,
Instrument
is
meant to express only
the
interface, and not a
particular implementation, so creating
an
Instrument
object
makes no sense, and you'll
probably want to
prevent
the
user from doing it.
This can be accomplished by
making all the
methods
in Instrument
print
error messages, but that
delays the
information
until run-time and requires
reliable exhaustive testing on
the
user's
part. It's always better to
catch problems at
compile-time.
Java
provides a mechanism for
doing this called the
abstract
method1.
This
is a method that is incomplete; it
has only a declaration and
no
method
body. Here is the syntax
for an abstract method
declaration:
abstract
void f();
A
class containing abstract
methods is called an abstract
class. If a
class
contains
one or more abstract
methods, the class must be
qualified as
abstract.
(Otherwise, the compiler
gives you an error
message.)
If
an abstract class is incomplete,
what is the compiler
supposed to do
when
someone tries to make an
object of that class? It
cannot safely create
an
object of an abstract class, so
you get an error message
from the
compiler.
This way the compiler
ensures the purity of the
abstract class,
and
you don't need to worry
about misusing it.
If
you inherit from an abstract
class and you want to
make objects of the
new
type, you must provide
method definitions for all
the abstract
methods
in the base class. If you
don't (and you may
choose not to),
then
1
For C++ programmers,
this is the analogue of C++'s
pure
virtual function.
326
Thinking
in Java
the
derived class is also
abstract and the compiler
will force you to
qualify
that
class
with the abstract
keyword.
It's
possible to create a class as
abstract
without
including any abstract
methods.
This is useful when you've
got a class in which it
doesn't make
sense
to have any abstract
methods,
and yet you want to
prevent any
instances
of that class.
The
Instrument
class
can easily be turned into an
abstract
class.
Only
some
of the methods will be
abstract,
since making a class
abstract
doesn't
force you to make all
the methods abstract.
Here's what it looks
like:
abstract
Instrument
abstract
void play();
String
what() { /* ... */ }
abstract
void adjust();
extends
extends
extends
Wind
Percussion
Stringed
void
play()
void
play()
void
play()
String
what()
String
what()
String
what()
void
adjust()
void
adjust()
void
adjust()
extends
extends
Woodwind
Brass
void
play()
void
play()
String
what()
void
adjust()
Here's
the orchestra example
modified to use abstract
classes
and
methods:
//:
c07:music4:Music4.java
Chapter
7: Polymorphism
327
//
Abstract classes and methods.
import
java.util.*;
abstract
class Instrument {
int
i; // storage allocated for each
public
abstract void play();
public
String what() {
return
"Instrument";
}
public
abstract void adjust();
}
class
Wind extends Instrument {
public
void play() {
System.out.println("Wind.play()");
}
public
String what() { return "Wind"; }
public
void adjust() {}
}
class
Percussion extends Instrument {
public
void play() {
System.out.println("Percussion.play()");
}
public
String what() { return "Percussion"; }
public
void adjust() {}
}
class
Stringed extends Instrument {
public
void play() {
System.out.println("Stringed.play()");
}
public
String what() { return "Stringed"; }
public
void adjust() {}
}
class
Brass extends Wind {
public
void play() {
System.out.println("Brass.play()");
}
public
void adjust() {
328
Thinking
in Java
System.out.println("Brass.adjust()");
}
}
class
Woodwind extends Wind {
public
void play() {
System.out.println("Woodwind.play()");
}
public
String what() { return "Woodwind"; }
}
public
class Music4 {
//
Doesn't care about type, so new types
//
added to the system still work right:
static
void tune(Instrument i) {
//
...
i.play();
}
static
void tuneAll(Instrument[] e) {
for(int
i = 0; i < e.length; i++)
tune(e[i]);
}
public
static void main(String[] args) {
Instrument[]
orchestra = new Instrument[5];
int
i = 0;
//
Upcasting during addition to the array:
orchestra[i++]
= new Wind();
orchestra[i++]
= new Percussion();
orchestra[i++]
= new Stringed();
orchestra[i++]
= new Brass();
orchestra[i++]
= new Woodwind();
tuneAll(orchestra);
}
}
///:~
You
can see that there's
really no change except in
the base class.
It's
helpful to create abstract
classes
and methods because they
make the
abstractness
of a class explicit, and
tell both the user
and the compiler
how
it was intended to be
used.
Chapter
7: Polymorphism
329
Constructors
and
polymorphism
As
usual, constructors are
different from other kinds
of methods. This is
also
true when polymorphism is
involved. Even though
constructors are
not
polymorphic (although you
can have a kind of "virtual
constructor," as
you
will see in Chapter 12),
it's important to understand
the way
constructors
work in complex hierarchies
and with polymorphism.
This
understanding
will help you avoid
unpleasant entanglements.
Order
of constructor calls
The
order of constructor calls
was briefly discussed in
Chapter 4 and
again
in Chapter 6, but that was
before polymorphism was
introduced.
A
constructor for the base
class is always called in
the constructor for a
derived
class, chaining up the
inheritance hierarchy so that a
constructor
for
every base class is called.
This makes sense because
the constructor
has
a special job: to see that
the object is built
properly. A derived
class
has
access to its own members
only, and not to those of
the base class
(whose
members are typically
private).
Only the base-class
constructor
has
the proper knowledge and
access to initialize its own
elements.
Therefore,
it's essential that all
constructors get called,
otherwise the
entire
object wouldn't be constructed.
That's why the compiler
enforces a
constructor
call for every portion of a
derived class. It will
silently call the
default
constructor if you don't
explicitly call a base-class
constructor in
the
derived-class constructor body. If
there is no default constructor,
the
compiler
will complain. (In the
case where a class has no
constructors, the
compiler
will automatically synthesize a
default constructor.)
Let's
take a look at an example
that shows the effects of
composition,
inheritance,
and polymorphism on the
order of construction:
//:
c07:Sandwich.java
//
Order of constructor calls.
class
Meal {
Meal()
{ System.out.println("Meal()"); }
330
Thinking
in Java
}
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:
Chapter
7: Polymorphism
331
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
This
means that the order of
constructor calls for a
complex object is as
follows:
1.
The
base-class constructor is called.
This step is repeated
recursively
such that the root of
the hierarchy is constructed
first,
followed
by the next-derived class,
etc., until the most-derived
class
is
reached.
2.
Member
initializers are called in
the order of
declaration.
3.
The
body of the derived-class
constructor is called.
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.
332
Thinking
in Java
Inheritance
and finalize(
)
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() throws Throwable {
System.out.println(
"LivingCreature
finalize");
Chapter
7: Polymorphism
333
//
Call base-class version LAST!
if(DoBaseFinalization.flag)
super.finalize();
}
}
class
Animal extends LivingCreature {
Characteristic
p =
new
Characteristic("has heart");
Animal()
{
System.out.println("Animal()");
}
protected
void finalize() throws Throwable {
System.out.println("Animal
finalize");
if(DoBaseFinalization.flag)
super.finalize();
}
}
class
Amphibian extends Animal {
Characteristic
p =
new
Characteristic("can live in water");
Amphibian()
{
System.out.println("Amphibian()");
}
protected
void finalize() throws Throwable {
System.out.println("Amphibian
finalize");
if(DoBaseFinalization.flag)
super.finalize();
}
}
public
class Frog extends Amphibian {
Frog()
{
System.out.println("Frog()");
}
protected
void finalize() throws Throwable {
System.out.println("Frog
finalize");
if(DoBaseFinalization.flag)
super.finalize();
}
334
Thinking
in Java
public
static void main(String[] args) {
if(args.length
!= 0 &&
args[0].equals("finalize"))
DoBaseFinalization.flag
= true;
else
System.out.println("Not
finalizing bases");
new
Frog(); // Instantly becomes garbage
System.out.println("Bye!");
//
Force finalizers to be called:
System.gc();
}
}
///:~
The
class DoBaseFinalization
simply
holds a flag that indicates
to each
class
in the hierarchy whether to
call super.finalize(
).
This flag is set
based
on a command-line argument, so you
can view the behavior
with
and
without base-class
finalization.
Each
class in the hierarchy also
contains a member object of
class
Characteristic.
You will see that
regardless of whether the
base class
finalizers
are called, the Characteristic
member
objects are always
finalized.
Each
overridden finalize(
) must
have access to at least
protected
members
since the finalize(
) method
in class Object
is
protected
and
the
compiler will not allow
you to reduce the access
during inheritance.
("Friendly"
is less accessible than
protected.)
In
Frog.main(
),
the DoBaseFinalization
flag
is configured and a
single
Frog object
is created. Remember that
garbage collection--and in
particular
finalization--might not happen
for any particular object,
so to
enforce
this, the call to System.gc(
) triggers
garbage collection,
and
thus
finalization. Without base-class
finalization, the output
is:
Not
finalizing bases
Creating
Characteristic is alive
LivingCreature()
Creating
Characteristic has heart
Animal()
Creating
Characteristic can live in water
Amphibian()
Chapter
7: Polymorphism
335
Frog()
Bye!
Frog
finalize
finalizing
Characteristic is alive
finalizing
Characteristic has heart
finalizing
Characteristic can live in water
You
can see that, indeed, no
finalizers are called for
the base classes of
Frog
(the
member objects are
finalized,
as you would expect). But if
you
add
the "finalize" argument on
the command line, you
get:
Creating
Characteristic is alive
LivingCreature()
Creating
Characteristic has heart
Animal()
Creating
Characteristic can live in water
Amphibian()
Frog()
bye!
Frog
finalize
Amphibian
finalize
Animal
finalize
LivingCreature
finalize
finalizing
Characteristic is alive
finalizing
Characteristic has heart
finalizing
Characteristic can live in water
Although
the order the member
objects are finalized is the
same order
that
they are created,
technically the order of
finalization of objects is
unspecified.
With base classes, however,
you have control over
the order
of
finalization. The best order
to use is the one that's
shown here, which is
the
reverse of the order of
initialization. Following the
form that's used in
C++
for destructors, you should
perform the derived-class
finalization
first,
then the base-class
finalization. That's because
the derived-class
finalization
could call some methods in
the base class that
require that the
base-class
components are still alive,
so you must not destroy
them
prematurely.
336
Thinking
in Java
Behavior
of polymorphic methods
inside
constructors
The
hierarchy of constructor calls
brings up an interesting dilemma.
What
happens
if you're inside a constructor
and you call a dynamically
bound
method
of the object being
constructed? Inside an ordinary
method you
can
imagine what will
happen--the dynamically bound
call is resolved at
run-time
because the object cannot
know whether it belongs to
the class
that
the method is in or some
class derived from it.
For consistency, you
might
think this is what should
happen inside
constructors.
This
is not exactly the case. If
you call a dynamically bound
method inside
a
constructor, the overridden
definition for that method
is used. However,
the
effect
can
be rather unexpected, and
can conceal some
difficult-to-find
bugs.
Conceptually,
the constructor's job is to
bring the object into
existence
(which
is hardly an ordinary feat).
Inside any constructor, the
entire
object
might be only partially
formed--you can know only
that the base-
class
objects have been
initialized, but you cannot
know which classes
are
inherited
from you. A dynamically
bound method call, however,
reaches
"outward"
into the inheritance
hierarchy. It calls a method in a
derived
class.
If you do this inside a
constructor, you call a
method that might
manipulate
members that haven't been
initialized yet--a sure
recipe for
disaster.
You
can see the problem in
the following
example:
//:
c07:PolyConstructors.java
//
Constructors and polymorphism
//
don't produce what you might expect.
abstract
class Glyph {
abstract
void draw();
Glyph()
{
System.out.println("Glyph()
before draw()");
draw();
System.out.println("Glyph()
after draw()");
}
}
Chapter
7: Polymorphism
337
class
RoundGlyph extends Glyph {
int
radius = 1;
RoundGlyph(int
r) {
radius
= r;
System.out.println(
"RoundGlyph.RoundGlyph(),
radius = "
+
radius);
}
void
draw() {
System.out.println(
"RoundGlyph.draw(),
radius = " + radius);
}
}
public
class PolyConstructors {
public
static void main(String[] args) {
new
RoundGlyph(5);
}
}
///:~
In
Glyph,
the draw(
) method
is abstract,
so it is designed to be
overridden.
Indeed, you are forced to
override it in RoundGlyph.
But
the
Glyph constructor
calls this method, and
the call ends up in
RoundGlyph.draw(
),
which would seem to be the
intent. But look at
the
output:
Glyph()
before draw()
RoundGlyph.draw(),
radius = 0
Glyph()
after draw()
RoundGlyph.RoundGlyph(),
radius = 5
When
Glyph's
constructor calls draw(
),
the value of radius
isn't
even
the
default initial value 1.
It's 0. This would probably
result in either a
dot
or
nothing at all being drawn
on the screen, and you'd be
left staring,
trying
to figure out why the
program won't work.
The
order of initialization described in
the previous section isn't
quite
complete,
and that's the key to
solving the mystery. The
actual process of
initialization
is:
338
Thinking
in Java
1.
The
storage allocated for the
object is initialized to binary
zero
before
anything else
happens.
2.
The
base-class constructors are
called as described previously.
At
this
point, the overridden
draw( )
method
is called (yes, before
the
RoundGlyph
constructor
is called), which discovers a
radius
value
of zero, due to step
1.
3.
Member
initializers are called in
the order of
declaration.
4.
The
body of the derived-class
constructor is called.
There's
an upside to this, which is
that everything is at least
initialized to
zero
(or whatever zero means
for that particular data
type) and not
just
left
as garbage. This includes
object references that are
embedded inside a
class
via composition, which
become null.
So if you forget to
initialize
that
reference you'll get an
exception at run-time. Everything
else gets
zero,
which is usually a telltale
value when looking at
output.
On
the other hand, you
should be pretty horrified at
the outcome of this
program.
You've done a perfectly
logical thing, and yet
the behavior is
mysteriously
wrong, with no complaints
from the compiler.
(C++
produces
more rational behavior in
this situation.) Bugs like
this could
easily
be buried and take a long
time to discover.
As
a result, a good guideline
for constructors is, "Do as
little as possible to
set
the object into a good
state, and if you can
possibly avoid it, don't
call
any
methods." The only safe
methods to call inside a
constructor are those
that
are final
in
the base class. (This
also applies to private
methods,
which
are automatically final.)
These cannot be overridden
and thus
cannot
produce this kind of
surprise.
Designing
with inheritance
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.
Chapter
7: Polymorphism
339
A
better approach is to choose
composition first, when it's
not obvious
which
one you should use.
Composition does not force a
design into an
inheritance
hierarchy. But composition is
also more flexible since
it's
possible
to dynamically choose a type
(and thus behavior) when
using
composition,
whereas inheritance requires an
exact type to be known
at
compile-time.
The following example
illustrates this:
//:
c07:Transmogrify.java
//
Dynamically changing the behavior of
//
an object via composition.
abstract
class Actor {
abstract
void act();
}
class
HappyActor extends Actor {
public
void act() {
System.out.println("HappyActor");
}
}
class
SadActor extends Actor {
public
void act() {
System.out.println("SadActor");
}
}
class
Stage {
Actor
a = new HappyActor();
void
change() { a = new SadActor(); }
void
go() { a.act(); }
}
public
class Transmogrify {
public
static void main(String[] args) {
Stage
s = new Stage();
s.go();
// Prints "HappyActor"
s.change();
s.go();
// Prints "SadActor"
}
}
///:~
340
Thinking
in Java
A
Stage
object
contains a reference to an Actor,
which is initialized to a
HappyActor
object.
This means go(
) produces
a particular behavior.
But
since a reference can be
rebound to a different object at
run-time, a
reference
for a SadActor
object
can be substituted in a
and
then the
behavior
produced by go(
) changes.
Thus you gain dynamic
flexibility at
run-time.
(This is also called the
State
Pattern. See
Thinking
in Patterns
with
Java, downloadable
at .)
In contrast, you
can't
decide
to inherit differently at run-time;
that must be
completely
determined
at compile-time.
A
general guideline is "Use
inheritance to express differences in
behavior,
and
fields to express variations in
state." In the above
example, both are
used:
two different classes are
inherited to express the
difference in the
act(
) method,
and Stage
uses
composition to allow its
state to be
changed.
In this case, that change in
state happens to produce a
change in
behavior.
Pure
inheritance vs. extension
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:
Shape
draw()
erase()
Circle
Square
Triangle
draw()
draw()
draw()
erase()
erase()
erase()
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
Chapter
7: Polymorphism
341
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:
Circle,
Square,
Talks
to Shape
Message
Line,
or new type
"Is-a"
of
Shape
relationship
That
is, the base class
can receive any message
you can send to
the
derived
class because the two
have exactly the same
interface. All you
need
to do is upcast from the
derived class and never
look back to see
what
exact type of object you're
dealing with. Everything is
handled
through
polymorphism.
When
you see it this way, it
seems like a pure "is-a"
relationship is the
only
sensible way to do things,
and any other design
indicates muddled
thinking
and is by definition broken.
This too is a trap. As soon
as you
start
thinking this way, you'll
turn around and discover
that extending the
interface
(which, unfortunately, the
keyword extends
seems
to
encourage)
is the perfect solution to a
particular problem. This
could be
termed
an "is-like-a" relationship because
the derived class is
like
the
base
class--it
has the same fundamental
interface--but it has other
features
that
require additional methods to
implement:
342
Thinking
in Java
Useful
Assume
this
}
void
f()
represents
a big
void
g()
interface
"Is-like-a"
MoreUseful
void
f()
void
g()
}
void
u()
Extending
void
v()
the
interface
void
w()
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:
Talks
to Useful
Useful
part
object
Message
MoreUseful
part
If
you're not upcasting in this
case, it won't bother you,
but often you'll
get
into
a situation in which you
need to rediscover the exact
type of the
object
so you can access the
extended methods of that
type. The following
section
shows how this is
done.
Downcasting
and run-time
type
identification
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
Chapter
7: Polymorphism
343
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.
Useful
Assume
this
}
void
f()
represents
a big
void
g()
interface
"Is-like-a"
MoreUseful
void
f()
void
g()
}
void
u()
Extending
void
v()
the
interface
void
w()
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() {}
344
Thinking
in Java
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
Chapter
7: Polymorphism
345
12
is devoted to the study of
different aspects of Java
run-time type
identification.
Summary
Polymorphism
means "different forms." In
object-oriented programming,
you
have the same face
(the common interface in the
base class) and
different
forms using that face:
the different versions of
the dynamically
bound
methods.
You've
seen in this chapter that
it's impossible to understand, or
even
create,
an example of polymorphism without
using data abstraction
and
inheritance.
Polymorphism is a feature that
cannot be viewed in
isolation
(like
a switch
statement
can, for example), but
instead works only in
concert,
as part of a "big picture" of
class relationships. People
are often
confused
by other, non-object-oriented features of
Java, like method
overloading,
which are sometimes
presented as object-oriented. Don't
be
fooled:
If it isn't late binding, it
isn't polymorphism.
To
use polymorphism--and thus
object-oriented
techniques--effectively
in
your programs you must
expand your view of
programming to include
not
just members and messages of
an individual class, but
also the
commonality
among classes and their
relationships with each
other.
Although
this requires significant
effort, it's a worthy
struggle, because
the
results are faster program
development, better code
organization,
extensible
programs, and easier code
maintenance.
Exercises
Solutions
to selected exercises can be
found in the electronic
document The
Thinking in Java
Annotated
Solution Guide, available
for a small fee from
.
1.
Add
a new method in the base
class of Shapes.java
that
prints a
message,
but don't override it in the
derived classes. Explain
what
happens.
Now override it in one of
the derived classes but
not the
others,
and see what happens.
Finally, override it in all
the derived
classes.
346
Thinking
in Java
2.
Add
a new type of Shape
to
Shapes.java
and
verify in main(
)
that
polymorphism works for your
new type as it does in the
old
types.
3.
Change
Music3.java
so
that what(
) becomes
the root Object
method
toString(
).
Try printing the Instrument
objects
using
System.out.println(
) (without
any casting).
4.
Add
a new type of Instrument
to
Music3.java
and
verify that
polymorphism
works for your new
type.
5.
Modify
Music3.java
so
that it randomly creates
Instrument
objects
the way Shapes.java
does.
6.
Create
an inheritance hierarchy of Rodent:
Mouse,
Gerbil,
Hamster,
etc. In the base class,
provide methods that
are
common
to all Rodents,
and override these in the
derived classes
to
perform different behaviors
depending on the specific
type of
Rodent.
Create an array of Rodent,
fill it with different
specific
types
of Rodents,
and call your base-class
methods to see what
happens.
7.
Modify
Exercise 6 so that Rodent
is
an abstract
class.
Make the
methods
of Rodent
abstract
whenever possible.
8.
Create
a class as abstract
without
including any abstract
methods,
and verify that you
cannot create any instances
of that
class.
9.
Add
class Pickle
to
Sandwich.java.
10.
Modify
Exercise 6 so that it demonstrates
the order of
initialization
of the base classes and
derived classes. Now
add
member
objects to both the base
and derived classes, and
show the
order
in which their initialization
occurs during
construction.
11.
Create
a 3-level inheritance hierarchy.
Each class in the
hierarchy
should
have a finalize(
) method,
and it should properly call
the
base-class
version of finalize(
).
Demonstrate that your
hierarchy
works
properly.
Chapter
7: Polymorphism
347
12.
Create
a base class with two
methods. In the first
method, call the
second
method. Inherit a class and
override the second
method.
Create
an object of the derived
class, upcast it to the base
type, and
call
the first method. Explain
what happens.
13.
Create
a base class with an
abstract
print( ) method
that is
overridden
in a derived class. The
overridden version of
the
method
prints the value of an
int variable
defined in the
derived
class.
At the point of definition of
this variable, give it a
nonzero
value.
In the base-class constructor,
call this method. In
main(
),
create
an object of the derived
type, and then call
its print(
)
method.
Explain the results.
14.
Following
the example in Transmogrify.java,
create a Starship
class
containing an AlertStatus
reference
that can indicate
three
different
states. Include methods to
change the states.
15.
Create
an abstract
class
with no methods. Derive a
class and add
a
method. Create a static
method
that takes a reference to
the
base
class, downcasts it to the
derived class, and calls
the method.
In
main(
),
demonstrate that it works.
Now put the abstract
declaration
for the method in the
base class, thus eliminating
the
need
for the downcast.
348
Thinking
in Java
8:
Interfaces &
Inner
Classes
Interfaces
and inner classes provide
more sophisticated
ways
to organize and control the
objects in your
system.
C++,
for example, does not
contain such mechanisms,
although the clever
programmer
may simulate them. The
fact that they exist in
Java indicates
that
they were considered
important enough to provide
direct support
through
language keywords.
In
Chapter 7, you learned about
the abstract
keyword,
which allows you
to
create one or more methods
in a class that have no
definitions--you
provide
part of the interface
without providing a
corresponding
implementation,
which is created by inheritors.
The interface
keyword
produces
a completely abstract class,
one that provides no
implementation
at all. You'll learn that
the interface
is
more than just an
abstract
class taken to the extreme,
since it allows you to
perform a
variation
on C++'s "multiple inheritance," by
creating a class that can
be
upcast
to more than one base
type.
At
first, inner classes look
like a simple code-hiding
mechanism: you place
classes
inside other classes. You'll
learn, however, that the
inner class
does
more than that--it knows
about and can communicate
with the
surrounding
class--and that the kind of
code you can write
with inner
classes
is more elegant and clear,
although it is a new concept to
most. It
takes
some time to become
comfortable with design
using inner classes.
Interfaces
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 fields,
but
349
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 so
that it is only
usable
within
the same package.
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 but
now I'm going to say
how it works."
Other
than that, it looks like
inheritance. The diagram for
the instrument
example
shows this:
350
Thinking
in Java
interface
Instrument
void
play();
String
what();
void
adjust();
implements
implements
implements
Wind
Percussion
Stringed
void
play()
void
play()
void
play()
String
what()
String
what()
String
what()
void
adjust()
void
adjust()
void
adjust()
extends
extends
Woodwind
Brass
void
play()
void
play()
String
what()
void
adjust()
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 reducing 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
Instrument
are
declared as public,
but they're automatically
public
anyway:
//:
c08:music5:Music5.java
Chapter
8: Interfaces & Inner
Classes
351
//
Interfaces.
import
java.util.*;
interface
Instrument {
//
Compile-time constant:
int
i = 5; // static & final
//
Cannot have method definitions:
void
play(); // Automatically public
String
what();
void
adjust();
}
class
Wind implements Instrument {
public
void play() {
System.out.println("Wind.play()");
}
public
String what() { return "Wind"; }
public
void adjust() {}
}
class
Percussion implements Instrument {
public
void play() {
System.out.println("Percussion.play()");
}
public
String what() { return "Percussion"; }
public
void adjust() {}
}
class
Stringed implements Instrument {
public
void play() {
System.out.println("Stringed.play()");
}
public
String what() { return "Stringed"; }
public
void adjust() {}
}
class
Brass extends Wind {
public
void play() {
System.out.println("Brass.play()");
}
public
void adjust() {
352
Thinking
in Java
System.out.println("Brass.adjust()");
}
}
class
Woodwind extends Wind {
public
void play() {
System.out.println("Woodwind.play()");
}
public
String what() { return "Woodwind"; }
}
public
class Music5 {
//
Doesn't care about type, so new types
//
added to the system still work right:
static
void tune(Instrument i) {
//
...
i.play();
}
static
void tuneAll(Instrument[] e) {
for(int
i = 0; i < e.length; i++)
tune(e[i]);
}
public
static void main(String[] args) {
Instrument[]
orchestra = new Instrument[5];
int
i = 0;
//
Upcasting during addition to the array:
orchestra[i++]
= new Wind();
orchestra[i++]
= new Percussion();
orchestra[i++]
= new Stringed();
orchestra[i++]
= new Brass();
orchestra[i++]
= new Woodwind();
tuneAll(orchestra);
}
}
///:~
The
rest of the code works
the same. It doesn't matter
if you are upcasting
to
a "regular" class called
Instrument,
an abstract
class
called
Instrument,
or to an interface
called
Instrument.
The behavior is the
same.
In fact, you can see in
the tune(
) method
that there isn't
any
evidence
about whether Instrument
is
a "regular" class, an abstract
Chapter
8: Interfaces & Inner
Classes
353
class,
or an interface.
This is the intent: Each
approach gives the
programmer
different control over the
way objects are created
and used.
"Multiple
inheritance" in Java
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:
interface
1
Abstract
or Concrete
Base
Class
...
interface
2
...
interface
n
...
Base
Class Functions
interface
1
interface
2
interface
n
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--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:
//:
c08:Adventure.java
//
Multiple interfaces.
import
java.util.*;
354
Thinking
in Java
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
h = new Hero();
t(h);
// Treat it as a CanFight
u(h);
// Treat it as a CanSwim
v(h);
// Treat it as a CanFly
w(h);
// 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.)
Chapter
8: Interfaces & Inner
Classes
355
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,
or if necessary a concrete
class.
Name
collisions when combining
interfaces
You
can encounter a small
pitfall when implementing
multiple interfaces.
In
the above example, both
CanFight
and
ActionCharacter
have
an
identical
void
fight( ) method.
This is no problem because
the method is
identical
in both cases, but what if
it's not? Here's an
example:
//:
c08:InterfaceCollision.java
356
Thinking
in Java
interface
I1
{ void f(); }
interface
I2
{ int f(int i); }
interface
I3
{ int f(); }
class
C {
public
int f() { return 1; } }
class
C2 implements I1, I2 {
public
void f() {}
public
int f(int i) { return 1; } // overloaded
}
class
C3 extends C implements I2 {
public
int f(int i) { return 1; } // overloaded
}
class
C4 extends C implements I3 {
//
Identical, no problem:
public
int f() { return 1; }
}
//
Methods differ only by return type:
//!
class C5 extends C implements I1 {}
//!
interface I4 extends I1, I3 {} ///:~
The
difficulty occurs because
overriding, implementation,
and
overloading
get unpleasantly mixed
together, and overloaded
functions
cannot
differ only by return type.
When the last two
lines are
uncommented,
the error messages say it
all:
InterfaceCollision.java:23:
f() in C cannot
implement
f() in I1; attempting to use
incompatible
return type
found
:
int
required:
void
InterfaceCollision.java:24:
interfaces I3 and I1 are
incompatible;
both define f
(),
but with different return type
Using
the same method names in
different interfaces that
are intended to
be
combined generally causes
confusion in the readability of
the code, as
well.
Strive to avoid it.
Chapter
8: Interfaces & Inner
Classes
357
Extending
an interface
with
inheritance
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:
//:
c08: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) {
358
Thinking
in Java
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.
Grouping
constants
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:
//:
c08:Months.java
//
Using interfaces to create groups of constants.
package
c08;
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
finals
that have
constant
initializers.
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
c08.*
or
c08.Months
just
as you would with any
other package, and
Chapter
8: Interfaces & Inner
Classes
359
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.
(That
approach
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 this1:
//:
c08:Month2.java
//
A more robust enumeration system.
package
c08;
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];
1
This approach
was inspired by an e-mail
from Rich Hoffarth.
360
Thinking
in Java
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 very
safe.
This
approach also allows you to
use ==
or
equals( )
interchangeably,
as
shown
at the end of main(
).
Initializing
fields in interfaces
Fields
defined in interfaces are
automatically static
and
final.
These
cannot
be "blank finals," but they
can be initialized with
nonconstant
expressions.
For example:
//:
c08: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;
}
///:~
Chapter
8: Interfaces & Inner
Classes
361
Since
the fields are static,
they are initialized when
the class is first
loaded,
which happens when any of
the fields are accessed
for the first
time.
Here's a simple test:
//:
c08: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.
Nesting
interfaces
2Interfaces
may
be nested within classes and
within other interfaces.
This
reveals
a number of very interesting
features:
//:
c08:NestingInterfaces.java
class
A {
interface
B {
void
f();
}
public
class BImp implements B {
public
void f() {}
}
private
class BImp2 implements B {
public
void f() {}
}
public
interface C {
void
f();
}
2
Thanks to
Martin Danner for asking
this question during a seminar.
362
Thinking
in Java
class
CImp implements C {
public
void f() {}
}
private
class CImp2 implements C {
public
void f() {}
}
private
interface D {
void
f();
}
private
class DImp implements D {
public
void f() {}
}
public
class DImp2 implements D {
public
void f() {}
}
public
D getD() { return new DImp2(); }
private
D dRef;
public
void receiveD(D d) {
dRef
= d;
dRef.f();
}
}
interface
E {
interface
G {
void
f();
}
//
Redundant "public":
public
interface H {
void
f();
}
void
g();
//
Cannot be private within an interface:
//!
private interface I {}
}
public
class NestingInterfaces {
public
class BImp implements A.B {
public
void f() {}
}
class
CImp implements A.C {
Chapter
8: Interfaces & Inner
Classes
363
public
void f() {}
}
//
Cannot implement a private interface except
//
within that interface's defining class:
//!
class DImp implements A.D {
//!
public void f() {}
//!
}
class
EImp implements E {
public
void g() {}
}
class
EGImp implements E.G {
public
void f() {}
}
class
EImp2 implements E {
public
void g() {}
class
EG implements E.G {
public
void f() {}
}
}
public
static void main(String[] args) {
A
a = new A();
//
Can't access A.D:
//!
A.D ad = a.getD();
//
Doesn't return anything but A.D:
//!
A.DImp2 di2 = a.getD();
//
Cannot access a member of the interface:
//!
a.getD().f();
//
Only another A can do anything with getD():
A
a2 = new A();
a2.receiveD(a.getD());
}
}
///:~
The
syntax for nesting an
interface within a class is
reasonably obvious,
and
just like non-nested
interfaces these can have
public
or
"friendly"
visibility.
You can also see
that both public
and
"friendly" nested
interfaces
can be implemented as a public,
"friendly," and private
nested
classes.
As
a new twist, interfaces can
also be private
as
seen in A.D
(the
same
qualification
syntax is used for nested
interfaces as for nested
classes).
364
Thinking
in Java
What
good is a private
nested
interface? You might guess
that it can only
be
implemented as a private
nested
class as in DImp,
but A.DImp2
shows
that it can also be
implemented as a public
class.
However,
A.DImp2
can
only be used as itself. You
are not allowed to mention
the
fact
that it implements the
private
interface,
so implementing a private
interface
is a way to force the
definition of the methods in
that interface
without
adding any type information
(that is, without allowing
any
upcasting).
The
method getD(
) produces
a further quandary concerning
the private
interface:
it's a public
method
that returns a reference to a
private
interface.
What can you do with
the return value of this
method? In
main(
),
you can see several
attempts to use the return
value, all of which
fail.
The only thing that
works is if the return value
is handed to an object
that
has permission to use it--in
this case, another A,
via the received(
)
method.
Interface
E shows
that interfaces can be
nested within each
other.
However,
the rules about
interfaces--in particular, that
all interface
elements
must be public--are
strictly enforced here, so an
interface
nested
within another interface is
automatically public
and
cannot be
made
private.
NestingInterfaces
shows
the various ways that
nested interfaces can
be
implemented.
In particular, notice that
when you implement an
interface,
you
are not required to
implement any interfaces
nested within. Also,
private
interfaces
cannot be implemented outside of
their defining
classes.
Initially,
these features may seem
like they are added
strictly for
syntactic
consistency,
but I generally find that
once you know about a
feature, you
often
discover places where it is
useful.
Inner
classes
It's
possible to place a class
definition within another
class definition.
This
is
called an inner
class. The
inner class is a valuable
feature because it
allows
you to group classes that
logically belong together
and to control
Chapter
8: Interfaces & Inner
Classes
365
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
examples 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:
//:
c08:Parcel1.java
//
Creating inner classes.
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);
System.out.println(d.readLabel());
}
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
366
Thinking
in Java
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 reference
to
an inner class, like
this:
//:
c08:Parcel2.java
//
Returning a reference to an inner class.
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);
System.out.println(d.readLabel());
}
public
static void main(String[] args) {
Parcel2
p = new Parcel2();
p.ship("Tanzania");
Parcel2
q = new Parcel2();
//
Defining references to inner classes:
Parcel2.Contents
c = q.cont();
Parcel2.Destination
d = q.to("Borneo");
}
}
///:~
Chapter
8: Interfaces & Inner
Classes
367
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(
).
Inner
classes and upcasting
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 reference from an
object that implements it
is
essentially
the same as upcasting to a
base class.) That's because
the inner
class--the
implementation of the interface--can
then be completely
unseen
and unavailable to anyone,
which is convenient for
hiding the
implementation.
All you get back is a
reference to the base class
or the
interface.
First,
the common interfaces will
be defined in their own
files so they can
be
used in all the
examples:
//:
c08:Destination.java
public
interface Destination {
String
readLabel();
}
///:~
//:
c08:Contents.java
public
interface Contents {
int
value();
}
///:~
Now
Contents
and
Destination
represent
interfaces available to
the
client
programmer. (The interface,
remember, automatically makes
all
of
its members public.)
When
you get back a reference to
the base class or the
interface,
it's
possible
that you can't even
find out the exact
type, as shown here:
//:
c08:Parcel3.java
//
Returning a reference to an inner class.
368
Thinking
in Java
public
class Parcel3 {
private
class PContents implements 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 pc = p.new
PContents();
}
}
///:~
Note
that since main(
) is
in Test,
when you want to run
this program
you
don't execute Parcel3,
but instead:
java
Test
In
the example, main(
) must
be in a separate class in order
to
demonstrate
the privateness of the inner
class PContents.
In
Parcel3,
something new has been
added: the inner class
PContents
is
private
so
no one but Parcel3
can
access it. PDestination
is
Chapter
8: Interfaces & Inner
Classes
369
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."
Inner
classes
in
methods and scopes
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:
1.
As
shown previously, you're
implementing an interface of
some
kind
so that you can create
and return a
reference.
2.
You're
solving a complicated problem
and you want to create
a
class
to aid in your solution, but
you don't want it
publicly
available.
In
the following examples, the
previous code will be
modified to use:
1.
A
class defined within a
method
370
Thinking
in Java
2.
A
class defined within a scope
inside a method
3.
An
anonymous class implementing an
interface
4.
An
anonymous class extending a
class that has a
nondefault
constructor
5.
An
anonymous class that
performs field
initialization
6.
An
anonymous class that
performs construction using
instance
initialization
(anonymous inner classes
cannot have
constructors)
Although
it's an ordinary class with
an implementation, Wrapping
is
also
being used as a common
"interface" to its derived
classes:
//:
c08:Wrapping.java
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):
//:
c08:Parcel4.java
//
Nesting a class within a method.
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);
}
Chapter
8: Interfaces & Inner
Classes
371
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 reference to
Destination,
the base class. 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.
The
next example shows how
you can nest an inner
class within any
arbitrary
scope:
//:
c08:Parcel5.java
//
Nesting a class within a scope.
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();
372
Thinking
in Java
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.
Anonymous
inner classes
The
next example looks a little
strange:
//:
c08:Parcel6.java
//
A method that returns an anonymous inner class.
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; }
Chapter
8: Interfaces & Inner
Classes
373
};
What
this strange syntax means
is: "Create an object of an
anonymous
class
that's inherited from
Contents."
The reference returned by
the new
expression
is automatically upcast to a Contents
reference.
The
anonymous
inner-class syntax is a shorthand
for:
class
MyContents implements 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:
//:
c08:Parcel7.java
//
An anonymous inner class that calls
//
the base-class constructor.
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(
).
374
Thinking
in Java
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:
//:
c08:Parcel8.java
//
An anonymous inner class that performs
//
initialization. A briefer version
//
of Parcel5.java.
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
instance
initialization,
you can, in effect, create a
constructor for an
anonymous
inner
class:
//:
c08:Parcel9.java
Chapter
8: Interfaces & Inner
Classes
375
//
Using "instance initialization" to perform
//
construction on an anonymous inner class.
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.
The
link to the outer
class
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,
an object
of
that inner class has a
link to the enclosing object
that made it, and so
it
can
access the members of that
enclosing object--without
any
special
qualifications.
In addition, inner classes
have access rights to all
the
376
Thinking
in Java
elements
in the enclosing class3.
The following example
demonstrates
this:
//:
c08:Sequence.java
//
Holds a sequence of Objects.
interface
Selector {
boolean
end();
Object
current();
void
next();
}
public
class Sequence {
private
Object[] obs;
private
int next = 0;
public
Sequence(int size) {
obs
= new Object[size];
}
public
void add(Object x) {
if(next
< obs.length) {
obs[next]
= x;
next++;
}
}
private
class SSelector implements Selector {
int
i = 0;
public
boolean end() {
return
i == obs.length;
}
public
Object current() {
return
obs[i];
}
public
void next() {
if(i
< obs.length) i++;
}
}
3
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++.
Chapter
8: Interfaces & Inner
Classes
377
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 obs,
which is a reference 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 automatic
access to the members of the
enclosing
class.
How can this happen?
The inner class must
keep a reference to
the
378
Thinking
in Java
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. Construction of
the inner class object
requires the
reference
to the object of the
enclosing class, and the
compiler will
complain
if it cannot access that
reference. Most of the time
this occurs
without
any intervention on the part
of the programmer.
static
inner
classes
If
you don't need a connection
between the inner class
object and the
outer
class object, then you
can make the inner
class static.
To
understand
the meaning of static
when
applied to inner classes,
you must
remember
that the object of an
ordinary inner class
implicitly keeps a
reference
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:
1.
You
don't need an outer-class
object in order to create an
object of
a
static
inner
class.
2.
You
can't access an outer-class
object from an object of a
static
inner
class.
static
inner
classes are different than
non-static
inner
classes in another
way,
as well. Fields and methods
in non-static
inner
classes can only be
at
the outer level of a class,
so non-static
inner
classes cannot have
static
data,
static
fields,
or static
inner
classes. However, static
inner
classes
can
have all of these:
//:
c08:Parcel10.java
//
Static inner classes.
public
class Parcel10 {
private
static class PContents
implements
Contents {
private
int i = 11;
public
int value() { return i; }
}
Chapter
8: Interfaces & Inner
Classes
379
Table of Contents:
|
|||||