|
|||||
}
}
class Circle
extends Shape {
Circle(int i)
{
super(i);
System.out.println("Drawing
a Circle");
}
void
cleanup() {
System.out.println("Erasing
a Circle");
super.cleanup();
}
}
class
Triangle extends Shape {
Triangle(int
i) {
super(i);
System.out.println("Drawing
a Triangle");
}
void
cleanup() {
System.out.println("Erasing
a Triangle");
super.cleanup();
}
}
class
Line extends Shape {
private
int start, end;
Line(int
start, int end) {
super(start);
this.start
= start;
this.end
= end;
System.out.println("Drawing
a Line: " +
start
+ ", " + end);
}
void
cleanup() {
System.out.println("Erasing
a Line: " +
start
+ ", " + end);
super.cleanup();
}
}
284
Thinking
in Java
public
class CADSystem extends Shape {
private
Circle c;
private
Triangle t;
private
Line[] lines = new Line[10];
CADSystem(int
i) {
super(i
+ 1);
for(int
j = 0; j < 10; j++)
lines[j]
= new Line(j, j*j);
c
= new Circle(1);
t
= new Triangle(1);
System.out.println("Combined
constructor");
}
void
cleanup() {
System.out.println("CADSystem.cleanup()");
//
The order of cleanup is the reverse
//
of the order of initialization
t.cleanup();
c.cleanup();
for(int
i = lines.length - 1; i >= 0; i--)
lines[i].cleanup();
super.cleanup();
}
public
static void main(String[] args) {
CADSystem
x = new CADSystem(47);
try
{
//
Code and exception handling...
}
finally {
x.cleanup();
}
}
}
///:~
Everything
in this system is some kind
of Shape
(which
is itself a kind of
Object
since
it's implicitly inherited
from the root class).
Each class
redefines
Shape's
cleanup(
) method
in addition to calling the
base-
class
version of that method using
super.
The specific Shape
classes--
Circle,
Triangle
and
Line--all
have constructors that
"draw," although
any
method called during the
lifetime of the object could
be responsible
for
doing something that needs
cleanup. Each class has
its own
cleanup(
) method
to restore nonmemory things
back to the way
they
were
before the object
existed.
Chapter
6: Reusing Classes
285
In
main(
),
you can see two
keywords that are new,
and won't officially
be
introduced until Chapter 10:
try and
finally.
The try
keyword
indicates
that the block that
follows (delimited by curly
braces) is a
guarded
region, which
means that it is given
special treatment. One
of
these
special treatments is that
the code in the finally
clause
following
this
guarded region is always
executed,
no matter how the try block
exits.
(With
exception handling, it's
possible to leave a try
block
in a number of
nonordinary
ways.) Here, the finally
clause
is saying "always
call
cleanup(
) for
x,
no matter what happens."
These keywords will
be
explained
thoroughly in Chapter
10.
Note
that in your cleanup method
you must also pay
attention to the
calling
order for the base-class
and member-object cleanup
methods in
case
one subobject depends on
another. In general, you
should follow the
same
form that is imposed by a
C++ compiler on its
destructors: First
perform
all of the cleanup work
specific to your class, in
the reverse order
of
creation. (In general, this
requires that base-class
elements still be
viable.)
Then call the base-class
cleanup method, as demonstrated
here.
There
can be many cases in which
the cleanup issue is not a
problem; you
just
let the garbage collector do
the work. But when
you must do it
explicitly,
diligence and attention is
required.
Order
of garbage collection
There's
not much you can
rely on when it comes to
garbage collection.
The
garbage
collector might never be
called. If it is, it can
reclaim objects in
any
order it wants. It's best to
not rely on garbage
collection for
anything
but
memory reclamation. If you
want cleanup to take place,
make your
own
cleanup methods and don't
rely on finalize(
).
(As mentioned in
Chapter
4, Java can be forced to
call all the
finalizers.)
Name
hiding
Only
C++ programmers might be
surprised by name hiding,
since it
works
differently in that language. If a
Java base class has a
method name
that's
overloaded several times,
redefining that method name
in the
derived
class will not
hide
any of the base-class
versions. Thus
overloading
works regardless of whether
the method was defined at
this
level
or in a base class:
286
Thinking
in Java
//:
c06:Hide.java
//
Overloading a base-class method name
//
in a derived class does not hide the
//
base-class versions.
class
Homer {
char
doh(char c) {
System.out.println("doh(char)");
return
'd';
}
float
doh(float f) {
System.out.println("doh(float)");
return
1.0f;
}
}
class
Milhouse {}
class
Bart extends Homer {
void
doh(Milhouse m) {}
}
class
Hide {
public
static void main(String[] args) {
Bart
b = new Bart();
b.doh(1);
// doh(float) used
b.doh('x');
b.doh(1.0f);
b.doh(new
Milhouse());
}
}
///:~
As
you'll see in the next
chapter, it's far more
common to override
methods
of the same name using
exactly the same signature
and return
type
as in the base class. It can
be confusing otherwise (which is
why C++
disallows
it, to prevent you from
making what is probably a
mistake).
Chapter
6: Reusing Classes
287
Choosing
composition
vs.
inheritance
Both
composition and inheritance
allow you to place
subobjects inside
your
new class. You might
wonder about the difference
between the two,
and
when to choose one over
the other.
Composition
is generally used when you
want the features of an
existing
class
inside your new class,
but not its interface.
That is, you embed
an
object
so that you can use it to
implement functionality in your
new class,
but
the user of your new
class sees the interface
you've defined for
the
new
class rather than the
interface from the embedded
object. For this
effect,
you embed private
objects
of existing classes inside
your new
class.
Sometimes
it makes sense to allow the
class user to directly
access the
composition
of your new class; that
is, to make the member
objects
public.
The member objects use
implementation hiding themselves,
so
this
is a safe thing to do. When
the user knows you're
assembling a bunch
of
parts, it makes the
interface easier to understand. A
car object
is a
good
example:
//:
c06:Car.java
//
Composition with public objects.
class
Engine {
public
void start() {}
public
void rev() {}
public
void stop() {}
}
class
Wheel {
public
void inflate(int psi) {}
}
class
Window {
public
void rollup() {}
public
void rolldown() {}
288
Thinking
in Java
}
class
Door {
public
Window window = new Window();
public
void open() {}
public
void close() {}
}
public
class Car {
public
Engine engine = new Engine();
public
Wheel[] wheel = new Wheel[4];
public
Door left = new Door(),
right
= new Door(); // 2-door
public
Car() {
for(int
i = 0; i < 4; i++)
wheel[i]
= new Wheel();
}
public
static void main(String[] args) {
Car
car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
}
///:~
Because
the composition of a car is
part of the analysis of the
problem
(and
not simply part of the
underlying design), making
the members
public
assists
the client programmer's
understanding of how to use
the
class
and requires less code
complexity for the creator
of the class.
However,
keep in mind that this is a
special case and that in
general you
should
make fields private.
When
you inherit, you take an
existing class and make a
special version of
it.
In general, this means that
you're taking a general-purpose
class and
specializing
it for a particular need.
With a little thought,
you'll see that it
would
make no sense to compose a
car using a vehicle
object--a car
doesn't
contain a vehicle, it is
a
vehicle. The is-a
relationship
is expressed
with
inheritance, and the
has-a
relationship
is expressed with
composition.
Chapter
6: Reusing Classes
289
protected
Now
that you've been introduced
to inheritance, the keyword
protected
finally
has meaning. In an ideal
world, private
members
would always be
hard-and-fast
private,
but in real projects there
are times when you
want
to
make something hidden from
the world at large and
yet allow access
for
members
of derived classes. The
protected
keyword
is a nod to
pragmatism.
It says "This is private
as
far as the class user is
concerned,
but
available to anyone who
inherits from this class or
anyone else in the
same
package."
That is, protected
in
Java is automatically
"friendly."
The
best tack to take is to
leave the data members
private--you
should
always
preserve your right to
change the underlying
implementation. You
can
then allow controlled access
to inheritors of your class
through
protected
methods:
//:
c06:Orc.java
//
The protected keyword.
import
java.util.*;
class
Villain {
private
int i;
protected
int read() { return i;
}
protected
void set(int ii) { i =
ii;
}
public
Villain(int ii) { i = ii;
}
public
int value(int m) { return
m*i;
}
}
public
class Orc extends Villain {
private
int j;
public
Orc(int jj) { super(jj); j = jj; }
public
void change(int x) { set(x); }
}
///:~
You
can see that change(
) has
access to set(
) because
it's protected.
290
Thinking
in Java
Incremental
development
One
of the advantages of inheritance is
that it supports incremental
development
by
allowing you to introduce
new code without causing
bugs
in
existing code. This also
isolates new bugs inside
the new code. By
inheriting
from an existing, functional
class and adding data
members
and
methods (and redefining
existing methods), you leave
the existing
code--that
someone else might still be
using--untouched and
unbugged.
If
a bug happens, you know
that it's in your new
code, which is much
shorter
and easier to read than if
you had modified the
body of existing
code.
It's
rather amazing how cleanly
the classes are separated.
You don't even
need
the source code for
the methods in order to
reuse the code. At
most,
you
just import a package. (This
is true for both inheritance
and
composition.)
It's
important to realize that
program development is an
incremental
process,
just like human learning.
You can do as much analysis
as you
want,
but you still won't
know all the answers
when you set out on
a
project.
You'll have much more
success--and more immediate
feedback--
if
you start out to "grow"
your project as an organic,
evolutionary creature,
rather
than constructing it all at
once like a glass-box
skyscraper.
Although
inheritance for experimentation
can be a useful technique,
at
some
point after things stabilize
you need to take a new
look at your class
hierarchy
with an eye to collapsing it
into a sensible structure.
Remember
that
underneath it all, inheritance is
meant to express a relationship
that
says
"This new class is a
type
of that
old class." Your program
should not
be
concerned with pushing bits
around, but instead with
creating and
manipulating
objects of various types to
express a model in the terms
that
come
from the problem
space.
Upcasting
The
most important aspect of
inheritance is not that it
provides methods
for
the new class. It's
the relationship expressed
between the new
class
Chapter
6: Reusing Classes
291
and
the base class. This
relationship can be summarized by
saying "The
new
class is
a type of the
existing class."
This
description is not just a
fanciful way of explaining
inheritance--it's
supported
directly by the language. As an
example, consider a base
class
called
Instrument
that
represents musical instruments,
and a derived
class
called Wind.
Because inheritance means
that all of the methods
in
the
base class are also
available in the derived
class, any message you
can
send
to the base class can
also be sent to the derived
class. If the
Instrument
class
has a play(
) method,
so will Wind
instruments.
This
means
we can accurately say that a
Wind object
is also a type of
Instrument.
The following example shows
how the compiler
supports
this
notion:
//:
c06:Wind.java
//
Inheritance & upcasting.
import
java.util.*;
class
Instrument {
public
void play() {}
static
void tune(Instrument i) {
//
...
i.play();
}
}
//
Wind objects are instruments
//
because they have the same interface:
class
Wind extends Instrument {
public
static void main(String[] args) {
Wind
flute = new Wind();
Instrument.tune(flute);
// Upcasting
}
}
///:~
What's
interesting in this example is
the tune(
) method,
which accepts
an
Instrument
reference.
However, in Wind.main( )
the
tune(
)
method
is called by giving it a Wind
reference.
Given that Java is
particular
about type checking, it
seems strange that a method
that
accepts
one type will readily
accept another type, until
you realize that a
Wind
object
is also an Instrument
object,
and there's no method
that
292
Thinking
in Java
tune(
) could
call for an Instrument
that
isn't also in Wind.
Inside
tune(
),
the code works for
Instrument
and
anything derived from
Instrument,
and the act of converting a
Wind reference
into an
Instrument
reference
is called upcasting.
Why
"upcasting"?
The
reason for the term is
historical, and based on the
way class
inheritance
diagrams have traditionally
been drawn: with the
root at the
top
of the page, growing
downward. (Of course, you
can draw your
diagrams
any way you find
helpful.) The inheritance
diagram for
Wind.java
is
then:
Instrument
Wind
Casting
from derived to base moves
up
on
the inheritance diagram, so
it's
commonly
referred to as upcasting.
Upcasting is always safe
because
you're
going from a more specific
type to a more general type.
That is, the
derived
class is a superset of the
base class. It might contain
more
methods
than the base class,
but it must contain
at
least the
methods in
the
base class. The only
thing that can occur to
the class interface
during
the
upcast is that it can lose
methods, not gain them.
This is why the
compiler
allows upcasting without any
explicit casts or other
special
notation.
You
can also perform the
reverse of upcasting, called
downcasting,
but
this
involves a dilemma that is
the subject of Chapter
12.
Composition
vs. inheritance
revisited
In
object-oriented programming, the
most likely way that
you'll create
and
use code is by simply
packaging data and methods
together into a
class,
and using objects of that
class. You'll also use
existing classes to
build
new classes with
composition. Less frequently,
you'll use
inheritance.
So although inheritance gets a
lot of emphasis while
learning
Chapter
6: Reusing Classes
293
OOP,
it doesn't mean that you
should use it everywhere you
possibly can.
On
the contrary, you should
use it sparingly, only when
it's clear that
inheritance
is useful. One of the
clearest ways to determine
whether you
should
use composition or inheritance is to
ask whether you'll ever
need
to
upcast from your new
class to the base class. If
you must upcast,
then
inheritance
is necessary, but if you
don't need to upcast, then
you should
look
closely at whether you need
inheritance. The next
chapter
(polymorphism)
provides one of the most
compelling reasons
for
upcasting,
but if you remember to ask
"Do I need to upcast?"
you'll have a
good
tool for deciding between
composition and
inheritance.
The
final
keyword
Java's
final keyword
has slightly different
meanings depending on
the
context,
but in general it says "This
cannot be changed." You
might want
to
prevent changes for two
reasons: design or efficiency.
Because these
two
reasons are quite different,
it's possible to misuse the
final keyword.
The
following sections discuss
the three places where
final can
be used:
for
data, methods, and
classes.
Final
data
Many
programming languages have a
way to tell the compiler
that a piece
of
data is "constant." A constant is
useful for two
reasons:
1.
It
can be a compile-time
constant that
won't ever change.
2.
It
can be a value initialized at
run-time that you don't
want
changed.
In
the case of a compile-time
constant, the compiler is
allowed to "fold"
the
constant value into any
calculations in which it's
used; that is,
the
calculation
can be performed at compile-time,
eliminating some
run-time
overhead.
In Java, these sorts of
constants must be primitives
and are
expressed
using the final
keyword.
A value must be given at the
time of
definition
of such a constant.
A
field that is both static
and
final has
only one piece of storage
that
cannot
be changed.
294
Thinking
in Java
When
using final
with
object references rather
than primitives the
meaning
gets a bit confusing. With a
primitive, final
makes
the value
a
constant,
but with an object
reference, final
makes
the reference
a
constant.
Once the reference is
initialized to an object, it can
never be
changed
to point to another object.
However, the object itself
can be
modified;
Java does not provide a
way to make any arbitrary
object a
constant.
(You can, however, write
your class so that objects
have the
effect
of being constant.) This
restriction includes arrays,
which are also
objects.
Here's
an example that demonstrates
final fields:
//:
c06:FinalData.java
//
The effect of final on fields.
class
Value {
int
i = 1;
}
public
class FinalData {
//
Can be compile-time constants
final
int i1 = 9;
static
final int VAL_TWO = 99;
//
Typical public constant:
public
static final int VAL_THREE = 39;
//
Cannot be compile-time constants:
final
int i4 = (int)(Math.random()*20);
static
final int i5 = (int)(Math.random()*20);
Value
v1 = new Value();
final
Value v2 = new Value();
static
final Value v3 = new Value();
//
Arrays:
final
int[] a = { 1, 2, 3, 4, 5, 6 };
public
void print(String id) {
System.out.println(
id
+ ": " + "i4 = " + i4 +
",
i5 = " + i5);
}
public
static void main(String[] args) {
Chapter
6: Reusing Classes
295
FinalData
fd1 = new FinalData();
//!
fd1.i1++; // Error: can't change value
fd1.v2.i++;
// Object isn't constant!
fd1.v1
= new Value(); // OK -- not final
for(int
i = 0; i < fd1.a.length; i++)
fd1.a[i]++;
// Object isn't constant!
//!
fd1.v2 = new Value(); // Error: Can't
//!
fd1.v3 = new Value(); // change reference
//!
fd1.a = new int[3];
fd1.print("fd1");
System.out.println("Creating
new FinalData");
FinalData
fd2 = new FinalData();
fd1.print("fd1");
fd2.print("fd2");
}
}
///:~
Since
i1 and
VAL_TWO
are
final primitives
with compile-time
values,
they
can both be used as
compile-time constants and
are not different in
any
important way. VAL_THREE
is
the more typical way
you'll see such
constants
defined: public
so
they're usable outside the
package, static
to
emphasize
that there's only one,
and final
to
say that it's a constant.
Note
that
final
static primitives
with constant initial values
(that is, compile-
time
constants) are named with
all capitals by convention,
with words
separated
by underscores (This is just
like C constants, which is
where the
convention
originated.) Also note that
i5 cannot
be known at compile-
time,
so it is not capitalized.
Just
because something is final
doesn't
mean that its value is
known at
compile-time.
This is demonstrated by initializing
i4 and
i5 at
run-time
using
randomly generated numbers.
This portion of the example
also
shows
the difference between
making a final
value
static
or
non-static.
This
difference shows up only
when the values are
initialized at run-time,
since
the compile-time values are
treated the same by the
compiler. (And
presumably
optimized out of existence.)
The difference is shown in
the
output
from one run:
fd1:
i4 = 15, i5 = 9
Creating
new FinalData
fd1:
i4 = 15, i5 = 9
296
Thinking
in Java
fd2:
i4 = 10, i5 = 9
Note
that the values of i4 for
fd1
and
fd2 are
unique, but the value
for i5
is
not changed by creating the
second FinalData
object.
That's because
it's
static
and
is initialized once upon
loading and not each
time a new
object
is created.
The
variables v1
through
v4 demonstrate
the meaning of a final
reference.
As you can see in main(
),
just because v2
is
final doesn't
mean
that you can't change
its value. However, you
cannot rebind v2
to
a
new
object, precisely because
it's final.
That's what final
means
for a
reference.
You can also see
the same meaning holds
true for an array,
which
is just another kind of
reference. (There is no way
that I know of to
make
the array references
themselves final.)
Making references final
seems
less useful than making
primitives final.
Blank
finals
Java
allows the creation of
blank
finals, which
are fields that are
declared
as
final but
are not given an
initialization value. In all
cases, the blank
final
must
be
initialized before it is used,
and the compiler ensures
this.
However,
blank finals provide much
more flexibility in the use
of the
final
keyword
since, for example, a
final field
inside a class can now
be
different
for each object and
yet it retains its immutable
quality. Here's an
example:
//:
c06:BlankFinal.java
//
"Blank" final data members.
class
Poppet { }
class
BlankFinal {
final
int i = 0; // Initialized final
final
int j; // Blank final
final
Poppet p; // Blank final reference
//
Blank finals MUST be initialized
//
in the constructor:
BlankFinal()
{
j
= 1; // Initialize blank final
p
= new Poppet();
}
Chapter
6: Reusing Classes
297
BlankFinal(int
x) {
j
= x; // Initialize blank final
p
= new Poppet();
}
public
static void main(String[] args) {
BlankFinal
bf = new BlankFinal();
}
}
///:~
You're
forced to perform assignments to
finals
either with an
expression
at
the point of definition of
the field or in every
constructor. This way
it's
guaranteed
that the final
field
is always initialized before
use.
Final
arguments
Java
allows you to make arguments
final by
declaring them as such in
the
argument
list. This means that
inside the method you
cannot change what
the
argument reference points
to:
//:
c06:FinalArguments.java
//
Using "final" with method arguments.
class
Gizmo {
public
void spin() {}
}
public
class FinalArguments {
void
with(final Gizmo g) {
//!
g = new Gizmo(); // Illegal -- g is final
}
void
without(Gizmo g) {
g
= new Gizmo(); // OK -- g not final
g.spin();
}
//
void f(final int i) { i++; } // Can't change
//
You can only read from a final primitive:
int
g(final int i) { return i + 1; }
public
static void main(String[] args) {
FinalArguments
bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
298
Thinking
in Java
}
///:~
Note
that you can still
assign a null
reference
to an argument that's
final
without
the compiler catching it,
just like you can
with a non-final
argument.
The
methods f(
) and
g( ) show
what happens when primitive
arguments
are
final:
you can read the
argument, but you can't
change it.
Final
methods
There
are two reasons for
final methods.
The first is to put a "lock"
on the
method
to prevent any inheriting
class from changing its
meaning. This is
done
for design reasons when
you want to make sure
that a method's
behavior
is retained during inheritance
and cannot be
overridden.
The
second reason for final methods
is efficiency. If you make a
method
final,
you are allowing the
compiler to turn any calls
to that method into
inline
calls.
When the compiler sees a
final method
call it can (at
its
discretion)
skip the normal approach of
inserting code to perform
the
method
call mechanism (push
arguments on the stack, hop
over to the
method
code and execute it,
hop back and clean
off the stack
arguments,
and
deal with the return
value) and instead replace
the method call with
a
copy
of the actual code in the
method body. This eliminates
the overhead
of
the method call. Of course,
if a method is big, then
your code begins to
bloat
and you probably won't
see any performance gains
from inlining,
since
any improvements will be
dwarfed by the amount of
time spent
inside
the method. It is implied
that the Java compiler is
able to detect
these
situations and choose wisely
whether to inline a final
method.
However,
it's better to not trust
that the compiler is able to
do this and
make
a method final
only
if it's quite small or if
you want to
explicitly
prevent
overriding.
final
and private
Any
private
methods
in a class are implicitly
final.
Because you can't
access
a private
method,
you can't override it (even
though the compiler
doesn't
give an error message if you
try to override it, you
haven't
overridden
the method, you've just
created a new method). You
can add
Chapter
6: Reusing Classes
299
the
final specifier
to a private
method
but it doesn't give that
method
any
extra meaning.
This
issue can cause confusion,
because if you try to
override a private
method
(which is implicitly final)
it seems to work:
//:
c06:FinalOverridingIllusion.java
//
It only looks like you can override
//
a private or private final method.
class
WithFinals {
//
Identical to "private" alone:
private
final void f() {
System.out.println("WithFinals.f()");
}
//
Also automatically "final":
private
void g() {
System.out.println("WithFinals.g()");
}
}
class
OverridingPrivate extends WithFinals {
private
final void f() {
System.out.println("OverridingPrivate.f()");
}
private
void g() {
System.out.println("OverridingPrivate.g()");
}
}
class
OverridingPrivate2
extends
OverridingPrivate {
public
final void f() {
System.out.println("OverridingPrivate2.f()");
}
public
void g() {
System.out.println("OverridingPrivate2.g()");
}
}
public
class FinalOverridingIllusion {
300
Thinking
in Java
public
static void main(String[] args) {
OverridingPrivate2
op2 =
new
OverridingPrivate2();
op2.f();
op2.g();
//
You can upcast:
OverridingPrivate
op = op2;
//
But you can't call the methods:
//!
op.f();
//!
op.g();
//
Same here:
WithFinals
wf = op2;
//!
wf.f();
//!
wf.g();
}
}
///:~
"Overriding"
can only occur if something
is part of the
base-class
interface.
That is, you must be
able to upcast an object to
its base type
and
call
the same method (the
point of this will become
clear in the next
chapter).
If a method is private,
it isn't part of the
base-class interface. It
is
just some code that's
hidden away inside the
class, and it just
happens
to
have that name, but if
you create a public,
protected
or
"friendly"
method
in the derived class,
there's no connection to the
method that
might
happen to have that name in
the base class. Since a
private
method
is unreachable and effectively
invisible, it doesn't factor
into
anything
except for the code
organization of the class
for which it was
defined.
Final
classes
When
you say that an entire
class is final
(by
preceding its definition
with
the
final keyword),
you state that you
don't want to inherit from
this class
or
allow anyone else to do so.
In other words, for some
reason the design
of
your class is such that
there is never a need to
make any changes, or
for
safety
or security reasons you
don't want subclassing.
Alternatively, you
might
be dealing with an efficiency
issue, and you want to
make sure that
any
activity involved with
objects of this class are as
efficient as possible.
//:
c06:Jurassic.java
//
Making an entire class final.
Chapter
6: Reusing Classes
301
class
SmallBrain {}
final
class Dinosaur {
int
i = 7;
int
j = 1;
SmallBrain
x = new SmallBrain();
void
f() {}
}
//!
class Further extends Dinosaur {}
//
error: Cannot extend final class 'Dinosaur'
public
class Jurassic {
public
static void main(String[] args) {
Dinosaur
n = new Dinosaur();
n.f();
n.i
= 40;
n.j++;
}
}
///:~
Note
that the data members
can be final
or
not, as you choose. The
same
rules
apply to final
for
data members regardless of
whether the class is
defined
as final.
Defining the class as
final simply
prevents inheritance--
nothing
more. However, because it
prevents inheritance all
methods in a
final
class
are implicitly final,
since there's no way to
override them. So
the
compiler has the same
efficiency options as it does if
you explicitly
declare
a method final.
You
can add the final
specifier
to a method in a final
class,
but it doesn't
add
any meaning.
Final
caution
It
can seem to be sensible to
make a method final
while
you're designing
a
class. You might feel
that efficiency is very
important when using
your
class
and that no one could
possibly want to override
your methods
anyway.
Sometimes this is
true.
302
Thinking
in Java
But
be careful with your
assumptions. In general, it's
difficult to anticipate
how
a class can be reused,
especially a general-purpose class. If
you define
a
method as final
you
might prevent the
possibility of reusing your
class
through
inheritance in some other
programmer's project simply
because
you
couldn't imagine it being
used that way.
The
standard Java library is a
good example of this. In
particular, the Java
1.0/1.1
Vector
class
was commonly used and
might have been even
more
useful
if, in the name of
efficiency, all the methods
hadn't been made
final.
It's easily conceivable that
you might want to inherit
and override
with
such a fundamentally useful
class, but the designers
somehow
decided
this wasn't appropriate.
This is ironic for two
reasons. First,
Stack
is
inherited from Vector,
which says that a Stack
is a
Vector,
which
isn't really true from a
logical standpoint. Second,
many of the most
important
methods of Vector,
such as addElement(
) and
elementAt(
) are
synchronized.
As you will see in Chapter
14, this
incurs
a significant performance overhead
that probably wipes out
any
gains
provided by final.
This lends credence to the
theory that
programmers
are consistently bad at
guessing where optimizations
should
occur.
It's just too bad
that such a clumsy design
made it into the
standard
library
where we must all cope
with it. (Fortunately, the
Java 2 container
library
replaces Vector
with
ArrayList,
which behaves much
more
civilly.
Unfortunately, there's still
plenty of new code being
written that
uses
the old container
library.)
It's
also interesting to note
that Hashtable,
another important
standard
library
class, does not
have
any final
methods.
As mentioned elsewhere
in
this book, it's quite
obvious that some classes
were designed by
completely
different people than
others. (You'll see that
the method
names
in Hashtable
are
much briefer compared to
those in Vector,
another
piece of evidence.) This is
precisely the sort of thing
that should
not
be
obvious to consumers of a class
library. When things
are
inconsistent
it just makes more work
for the user. Yet
another paean to
the
value of design and code
walkthroughs. (Note that the
Java 2
container
library replaces Hashtable
with
HashMap.)
Chapter
6: Reusing Classes
303
Initialization
and
class
loading
In
more traditional languages,
programs are loaded all at
once as part of
the
startup process. This is
followed by initialization, and
then the
program
begins. The process of
initialization in these languages
must be
carefully
controlled so that the order
of initialization of statics
doesn't
cause
trouble. C++, for example,
has problems if one
static
expects
another
static
to
be valid before the second
one has been
initialized.
Java
doesn't have this problem
because it takes a different
approach to
loading.
Because everything in Java is an
object, many activities
become
easier,
and this is one of them. As
you will learn more
fully in the next
chapter,
the compiled code for
each class exists in its
own separate file.
That
file isn't loaded until
the code is needed. In
general, you can say
that
"Class
code is loaded at the point
of first use." This is often
not until the
first
object of that class is
constructed, but loading
also occurs when a
static
field
or static
method
is accessed.
The
point of first use is also
where the static
initialization
takes place. All
the
static
objects
and the static
code
block will be initialized in
textual
order
(that is, the order
that you write them
down in the class
definition)
at
the point of loading. The
statics,
of course, are initialized
only once.
Initialization
with inheritance
It's
helpful to look at the whole
initialization process,
including
inheritance,
to get a full picture of
what happens. Consider the
following
code:
//:
c06:Beetle.java
//
The full process of initialization.
class
Insect {
int
i = 9;
int
j;
Insect()
{
prt("i
= " + i + ", j = " + j);
304
Thinking
in Java
j
= 39;
}
static
int x1 =
prt("static
Insect.x1 initialized");
static
int prt(String s) {
System.out.println(s);
return
47;
}
}
public
class Beetle extends Insect {
int
k = prt("Beetle.k initialized");
Beetle()
{
prt("k
= " + k);
prt("j
= " + j);
}
static
int x2 =
prt("static
Beetle.x2 initialized");
public
static void main(String[] args) {
prt("Beetle
constructor");
Beetle
b = new Beetle();
}
}
///:~
The
output for this program
is:
static
Insect.x1 initialized
static
Beetle.x2 initialized
Beetle
constructor
i
= 9, j = 0
Beetle.k
initialized
k
= 47
j
= 39
The
first thing that happens
when you run Java on
Beetle
is
that you try
to
access Beetle.main(
) (a
static
method),
so the loader goes out
and
finds
the compiled code for
the Beetle
class
(this happens to be in a
file
called
Beetle.class).
In the process of loading
it, the loader notices
that it
has
a base class (that's what
the extends
keyword
says), which it then
loads.
This will happen whether or
not you're going to make an
object of
Chapter
6: Reusing Classes
305
that
base class. (Try commenting
out the object creation to
prove it to
yourself.)
If
the base class has a
base class, that second
base class would then
be
loaded,
and so on. Next, the
static
initialization
in the root base class
(in
this
case, Insect)
is performed, and then the
next derived class, and
so
on.
This is important because
the derived-class static
initialization might
depend
on the base class member
being initialized
properly.
At
this point, the necessary
classes have all been
loaded so the object
can
be
created. First, all the
primitives in this object
are set to their
default
values
and the object references
are set to null--this
happens in one fell
swoop
by setting the memory in the
object to binary zero. Then
the base-
class
constructor will be called. In
this case the call is
automatic, but you
can
also specify the base-class
constructor call (as the
first operation in
the
Beetle( )
constructor)
using super.
The base class construction
goes
through
the same process in the
same order as the
derived-class
constructor.
After the base-class
constructor completes, the
instance
variables
are initialized in textual
order. Finally, the rest of
the body of the
constructor
is executed.
Summary
Both
inheritance and composition
allow you to create a new
type from
existing
types. Typically, however,
you use composition to reuse
existing
types
as part of the underlying
implementation of the new
type, and
inheritance
when you want to reuse
the interface. Since the
derived class
has
the base-class interface, it
can be upcast
to
the base, which is
critical
for
polymorphism, as you'll see in
the next chapter.
Despite
the strong emphasis on
inheritance in object-oriented
programming,
when you start a design
you should generally
prefer
composition
during the first cut
and use inheritance only
when it is clearly
necessary.
Composition tends to be more
flexible. In addition, by
using
the
added artifice of inheritance
with your member type,
you can change
the
exact type, and thus
the behavior, of those
member objects at
run-
time.
Therefore, you can change
the behavior of the composed
object at
run-time.
306
Thinking
in Java
Although
code reuse through
composition and inheritance is
helpful for
rapid
project development, you'll
generally want to redesign
your class
hierarchy
before allowing other
programmers to become dependent on
it.
Your
goal is a hierarchy in which
each class has a specific
use and is
neither
too big (encompassing so
much functionality that it's
unwieldy to
reuse)
nor annoyingly small (you
can't use it by itself or
without adding
functionality).
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.
Create
two classes, A
and
B,
with default constructors
(empty
argument
lists) that announce
themselves. Inherit a new
class
called
C from
A,
and create a member of class
B inside
C.
Do not
create
a constructor for C.
Create an object of class
C and
observe
the
results.
2.
Modify
Exercise 1 so that A
and
B have
constructors with
arguments
instead of default constructors.
Write a constructor
for
C
and
perform all initialization
within C's
constructor.
3.
Create
a simple class. Inside a
second class, define a field
for an
object
of the first class. Use
lazy initialization to instantiate
this
object.
4.
Inherit
a new class from class
Detergent.
Override scrub(
) and
add
a new method called
sterilize(
).
5.
Take
the file Cartoon.java
and
comment out the constructor
for
the
Cartoon
class.
Explain what happens.
6.
Take
the file Chess.java
and
comment out the constructor
for the
Chess
class.
Explain what happens.
7.
Prove
that default constructors
are created for you by
the
compiler.
8.
Prove
that the base-class
constructors are (a) always
called, and
(b)
called before derived-class
constructors.
Chapter
6: Reusing Classes
307
9.
Create
a base class with only a
nondefault constructor, and
a
derived
class with both a default
and nondefault constructor.
In
the
derived-class constructors, call
the base-class
constructor.
10.
Create
a class called Root
that
contains an instance of each
of
classes
(that you also create)
named Component1,
Component2,
and Component3.
Derive a class Stem
from
Root
that
also contains an instance of
each "component." All
classes
should have default
constructors that print a
message
about
that class.
11.
Modify
Exercise 10 so that each
class only has
nondefault
constructors.
12.
Add
a proper hierarchy of cleanup(
) methods
to all the classes in
Exercise
11.
13.
Create
a class with a method that
is overloaded three
times.
Inherit
a new class, add a new
overloading of the method,
and
show
that all four methods
are available in the derived
class.
14.
In
Car.java
add
a service(
) method
to Engine
and
call this
method
in main(
).
15.
Create
a class inside a package.
Your class should contain
a
protected
method.
Outside of the package, try
to call the
protected
method
and explain the results.
Now inherit from
your
class
and call the protected
method
from inside a method of
your
derived
class.
16.
Create
a class called Amphibian.
From this, inherit a class
called
Frog.
Put appropriate methods in
the base class. In main(
),
create
a Frog
and
upcast it to Amphibian,
and demonstrate that
all
the methods still
work.
17.
Modify
Exercise 16 so that Frog
overrides
the method
definitions
from
the base class (provides
new definitions using the
same
method
signatures). Note what
happens in main(
).
18.
Create
a class with a static
final field
and a final
field
and
demonstrate
the difference between the
two.
308
Thinking
in Java
19.
Create
a class with a blank
final reference
to an object. Perform
the
initialization of the blank
final inside
a method (not the
constructor)
right before you use
it. Demonstrate the
guarantee
that
the final
must
be initialized before use,
and that it cannot be
changed
once initialized.
20.
Create
a class with a final
method.
Inherit from that class
and
attempt
to override that
method.
21.
Create
a final
class
and attempt to inherit from
it.
22.
Prove
that class loading takes
place only once. Prove
that loading
may
be caused by either the
creation of the first
instance of that
class,
or the access of a static
member.
23.
In
Beetle.java,
inherit a specific type of
beetle from class
Beetle,
following the same format as
the existing classes.
Trace
and
explain the output.
Chapter
6: Reusing Classes
309
7:
Polymorphism
Polymorphism
is the third essential
feature of an object-
oriented
programming language, after
data abstraction
and
inheritance.
It
provides another dimension of
separation of interface
from
implementation,
to decouple what
from
how.
Polymorphism allows
improved
code organization and
readability as well as the
creation of
extensible
programs
that can be "grown" not
only during the
original
creation
of the project but also
when new features are
desired.
Encapsulation
creates new data types by
combining characteristics
and
behaviors.
Implementation hiding separates
the interface from
the
implementation
by making the details
private.
This sort of
mechanical
organization
makes ready sense to someone
with a procedural
programming
background. But polymorphism
deals with decoupling
in
terms
of types.
In the last chapter, you
saw how inheritance allows
the
treatment
of an object as its own type
or
its
base type. This ability
is
critical
because it allows many types
(derived from the same
base type) to
be
treated as if they were one
type, and a single piece of
code to work on
all
those different types
equally. The polymorphic
method call allows
one
type
to express its distinction
from another, similar type,
as long as
they're
both derived from the
same base type. This
distinction is
expressed
through differences in behavior of
the methods that you
can
call
through the base
class.
In
this chapter, you'll learn
about polymorphism (also
called dynamic
binding
or
late
binding or run-time
binding) starting
from the basics,
with
simple examples that strip
away everything but the
polymorphic
behavior
of the program.
Upcasting
revisited
In
Chapter 6 you saw how an
object can be used as its
own type or as an
object
of its base type. Taking an
object reference and
treating it as a
311
reference
to its base type is called
upcasting,
because
of the way
inheritance
trees are drawn with
the base class at the
top.
You
also saw a problem arise,
which is embodied in the
following:
//:
c07:music:Music.java
//
Inheritance & upcasting.
class
Note {
private
int value;
private
Note(int val) { value = val; }
public
static final Note
MIDDLE_C
= new Note(0),
C_SHARP
= new Note(1),
B_FLAT
=
new Note(2);
}
// Etc.
class
Instrument {
public
void play(Note n) {
System.out.println("Instrument.play()");
}
}
//
Wind objects are instruments
//
because they have the same interface:
class
Wind extends Instrument {
//
Redefine interface method:
public
void play(Note n) {
System.out.println("Wind.play()");
}
}
public
class Music {
public
static void tune(Instrument i) {
//
...
i.play(Note.MIDDLE_C);
}
public
static void main(String[] args) {
Wind
flute = new Wind();
tune(flute);
// Upcasting
}
312
Thinking
in Java
}
///:~
The
method Music.tune(
) accepts
an Instrument
reference,
but also
anything
derived from Instrument.
In main(
),
you can see
this
happening
as a Wind
reference
is passed to tune(
),
with no cast
necessary.
This is acceptable; the
interface in Instrument
must
exist in
Wind,
because Wind
is
inherited from Instrument.
Upcasting from
Wind
to
Instrument
may
"narrow" that interface, but
it cannot make it
anything
less than the full
interface to Instrument.
Forgetting
the object type
This
program might seem strange
to you. Why should
anyone
intentionally
forget
the
type of an object? This is
what happens when
you
upcast,
and it seems like it could
be much more straightforward if
tune(
)
simply
takes a Wind
reference
as its argument. This brings
up an
essential
point: If you did that,
you'd need to write a new
tune( )
for
every
type of Instrument
in
your system. Suppose we
follow this
reasoning
and add Stringed
and
Brass
instruments:
//:
c07:music2:Music2.java
//
Overloading instead of upcasting.
class
Note {
private
int value;
private
Note(int val) { value = val; }
public
static final Note
MIDDLE_C
= new Note(0),
C_SHARP
= new Note(1),
B_FLAT
= new Note(2);
}
// Etc.
class
Instrument {
public
void play(Note n) {
System.out.println("Instrument.play()");
}
}
class
Wind extends Instrument {
public
void play(Note n) {
System.out.println("Wind.play()");
Chapter
7: Polymorphism
313
}
}
class
Stringed extends Instrument {
public
void play(Note n) {
System.out.println("Stringed.play()");
}
}
class
Brass extends Instrument {
public
void play(Note n) {
System.out.println("Brass.play()");
}
}
public
class Music2 {
public
static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public
static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public
static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public
static void main(String[] args) {
Wind
flute = new Wind();
Stringed
violin = new Stringed();
Brass
frenchHorn = new Brass();
tune(flute);
// No upcasting
tune(violin);
tune(frenchHorn);
}
}
///:~
This
works, but there's a major
drawback: You must write
type-specific
methods
for each new Instrument
class
you add. This means
more
programming
in the first place, but it
also means that if you
want to add a
new
method like tune(
) or
a new type of Instrument,
you've got a lot of
work
to do. Add the fact
that the compiler won't
give you any
error
314
Thinking
in Java
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
twist
The
difficulty with Music.java
can
be seen by running the
program. The
output
is Wind.play(
).
This is clearly the desired
output, but it
doesn't
seem
to make sense that it would
work that way. Look at
the tune(
)
method:
public
static void tune(Instrument i) {
//
...
i.play(Note.MIDDLE_C);
}
It
receives an Instrument
reference.
So how can the compiler
possibly
know
that this Instrument
reference
points to a Wind
in
this case and
not
a Brass
or
Stringed?
The compiler can't. To get a
deeper
understanding
of the issue, it's helpful
to examine the subject of
binding.
Method-call
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.
Chapter
7: Polymorphism
315
The
confusing part of the above
program revolves around
early binding
because
the compiler cannot know
the correct method to call
when it has
only
an Instrument
reference.
The
solution is called late
binding, which
means that the binding
occurs
at
run-time based on the type
of object. Late binding is
also called
dynamic
binding or run-time
binding. When a
language implements
late
binding,
there must be some mechanism
to determine the type of
the
object
at run-time and to call the
appropriate method. That is,
the
compiler
still doesn't know the
object type, but the
method-call
mechanism
finds out and calls
the correct method body.
The late-binding
mechanism
varies from language to
language, but you can
imagine that
some
sort of type information
must be installed in the
objects.
All
method binding in Java uses
late binding unless a method
has been
declared
final.
This means that ordinarily
you don't need to make
any
decisions
about whether late binding
will occur--it
happens
automatically.
Why
would you declare a method
final?
As noted in the last
chapter, it
prevents
anyone from overriding that
method. Perhaps more
important, it
effectively
"turns off" dynamic binding,
or rather it tells the
compiler that
dynamic
binding isn't necessary.
This allows the compiler to
generate
slightly
more efficient code for
final method
calls. However, in
most
cases
it won't make any overall
performance difference in your
program,
so
it's best to only use
final as
a design decision, and not
as an attempt to
improve
performance.
Producing
the right behavior
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.
316
Thinking
in Java
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:
Shape
Cast
"up" the
inheritance
draw()
diagram
erase()
Circle
Square
Triangle
Circle
draw()
draw()
draw()
Handle
erase()
erase()
erase()
The
upcast could occur in a
statement as simple
as:
Shape
s = new Circle();
Here,
a Circle
object
is created and the resulting
reference is immediately
assigned
to a Shape,
which would seem to be an
error (assigning one
type
to
another); and yet it's
fine because a Circle
is a
Shape
by
inheritance.
So
the compiler agrees with
the statement and doesn't
issue an error
message.
Suppose
you call one of the
base-class methods (that
have been
overridden
in the derived
classes):
s.draw();
Again,
you might expect that
Shape's
draw( )
is
called because this
is,
after
all, a Shape
reference--so
how could the compiler
know to do
anything
else? And yet the
proper Circle.draw(
) is
called because of
late
binding
(polymorphism).
The
following example puts it a
slightly different
way:
//:
c07:Shapes.java
Chapter
7: Polymorphism
317
//
Polymorphism in Java.
class
Shape {
void
draw() {}
void
erase() {}
}
class
Circle extends Shape {
void
draw() {
System.out.println("Circle.draw()");
}
void
erase() {
System.out.println("Circle.erase()");
}
}
class
Square extends Shape {
void
draw() {
System.out.println("Square.draw()");
}
void
erase() {
System.out.println("Square.erase()");
}
}
class
Triangle extends Shape {
void
draw() {
System.out.println("Triangle.draw()");
}
void
erase() {
System.out.println("Triangle.erase()");
}
}
public
class Shapes {
public
static Shape randShape() {
switch((int)(Math.random()
* 3)) {
default:
case
0: return new Circle();
case
1: return new Square();
case
2: return new Triangle();
318
Thinking
in Java
}
}
public
static void main(String[] args) {
Shape[]
s = new Shape[9];
//
Fill up the array with shapes:
for(int
i = 0; i < s.length; i++)
s[i]
= randShape();
//
Make polymorphic method calls:
for(int
i = 0; i < s.length; i++)
s[i].draw();
}
}
///:~
The
base class Shape
establishes
the common interface to
anything
inherited
from Shape--that
is, all shapes can be
drawn and erased.
The
derived
classes override these
definitions to provide unique
behavior for
each
specific type of
shape.
The
main class Shapes
contains
a static
method
randShape(
) that
produces
a reference to a randomly-selected
Shape
object
each time you
call
it. Note that the
upcasting happens in each of
the return
statements,
which
take a reference to a Circle,
Square,
or Triangle
and
sends it out
of
the method as the return
type, Shape.
So whenever you call
this
method
you never get a chance to
see what specific type it
is, since you
always
get back a plain Shape
reference.
main(
) contains
an array of Shape
references
filled through calls
to
randShape(
).
At this point you know
you have Shapes,
but you don't
know
anything more specific than
that (and neither does
the compiler).
However,
when you step through
this array and call
draw( )
for
each one,
the
correct type-specific behavior
magically occurs, as you can
see from
one
output example:
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Chapter
7: Polymorphism
319
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.
Extensibility
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:
320
Thinking
in Java
Instrument
void
play()
String
what()
void
adjust()
Wind
Percussion
Stringed
void
play()
void
play()
void
play()
String
what()
String
what()
String
what()
void
adjust()
void
adjust()
void
adjust()
Woodwind
Brass
void
play()
void
play()
String
what()
void
adjust()
All
these new classes work
correctly with the old,
unchanged tune(
)
method.
Even if tune(
) is
in a separate file and new
methods are added
to
the interface of Instrument,
tune( )
works
correctly without
recompilation.
Here is the implementation of
the above diagram:
//:
c07:music3:Music3.java
//
An extensible program.
import
java.util.*;
class
Instrument {
public
void play() {
System.out.println("Instrument.play()");
}
public
String what() {
return
"Instrument";
}
public
void adjust() {}
Chapter
7: Polymorphism
321
Table of Contents:
|
|||||