ZeePedia

Polymorphism:Upcasting revisited, The twist, Designing with inheritance

<< Reusing Classes:Composition syntax, Combining composition and inheritance
Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers >>
img
}
}
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
img
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
img
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
img
//: 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
img
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
img
}
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
} ///:~
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
} ///:~
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
img
}
}
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
img
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
img
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
img
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
img
// 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
img
}
}
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
img
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
img
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:
  1. Introduction to Objects:The progress of abstraction, An object has an interface
  2. Everything is an Object:You manipulate objects with references, Your first Java program
  3. Controlling Program Flow:Using Java operators, Execution control, true and false
  4. Initialization & Cleanup:Method overloading, Member initialization
  5. Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation
  6. Reusing Classes:Composition syntax, Combining composition and inheritance
  7. Polymorphism:Upcasting revisited, The twist, Designing with inheritance
  8. Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers
  9. Holding Your Objects:Container disadvantage, List functionality, Map functionality
  10. Error Handling with Exceptions:Basic exceptions, Catching an exception
  11. The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
  12. Run-time Type Identification:The need for RTTI, A class method extractor
  13. Creating Windows & Applets:Applet restrictions, Running applets from the command line
  14. Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
  15. Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans
  16. A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects
  17. B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
  18. Java Programming Guidelines:Design, Implementation
  19. Resources:Software, Books, My own list of books
  20. Index