Constructors
and polymorphism
As
usual, constructors
are different from other kinds of methods. This is also true when polymorphism
is involved. Even though constructors are not polymorphic (although you can
have a kind of “virtual constructor,” as you will see in Chapter
11), it’s important to understand the way constructors work in complex
hierarchies and with polymorphism. This understanding will help you avoid
unpleasant entanglements.
Order
of constructor calls
The
order of constructor calls was briefly discussed in Chapter 4, but that was
before inheritance and polymorphism were introduced.
A
constructor for the base class is always called in the constructor for a
derived class, chaining upward so that a constructor for every base class is
called. This makes sense because the constructor has a special job: to see that
the object is built properly. A derived class has access to its own members
only, and not to those of the base class (whose members are typically
private).
Only the base-class constructor has the proper knowledge and access to
initialize its own elements. Therefore, it’s essential that all
constructors get called, otherwise the entire object wouldn’t be
constructed properly. That’s why the compiler enforces a constructor call
for every portion of a derived class. It will silently call the default
constructor if you don’t explicitly call a base-class constructor in the
derived-class constructor body. If there is no default constructor, the
compiler will complain. (In the case where a class has no constructors, the
compiler will automatically synthesize a default constructor.)
Let’s
take a look at an example that shows the effects of composition, inheritance,
and polymorphism on the order of construction:
//: Sandwich.java
// Order of constructor calls
class Meal {
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()");}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
class Sandwich extends PortableLunch {
Bread b = new Bread();
Cheese c = new Cheese();
Lettuce l = new Lettuce();
Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~
This
example creates a complex class out of other classes, and each class has a
constructor that announces itself. The important class is
Sandwich,
which reflects three levels of inheritance (four, if you count the implicit
inheritance from
Object)
and three member objects. When a
Sandwich
object is created in
main( ),
the output is:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
This
means that the order of constructor calls for a complex object is as follows:
- The
base-class
constructor is called. This step is repeated recursively such that the root of
the hierarchy is constructed first, followed by the next-derived class, etc.,
until the most-derived class is reached.
- Member
initializers are called in the order of declaration.
- The
body of the derived-class constructor is called.
The
order of the constructor calls is important. When you inherit, you know all
about the base class and can access any
public
and
protected
members of the base class. This means that you must be able to assume that all
the members of the base class are valid when you’re in the derived class.
In a normal method, construction has already taken place, so all the members of
all parts of the object have been built.
Inside
the constructor, however, you must be able to assume that all members that you
use have been built. The only way to guarantee this is for the base-class
constructor to be called first. Then when you’re in the derived-class
constructor, all the members you can access in the base class have been
initialized. “Knowing that all members are valid” inside the
constructor is also the reason that, whenever possible, you should initialize
all member objects (that is, objects placed in the class using composition) at
their point of definition in the class (e.g.:
b,
c,
and
l
in the example above). If you follow this practice, you will help ensure that
all base class members
and
member objects of the current object have been initialized. Unfortunately, this
doesn’t handle every case, as you will see in the next section.
Inheritance
and finalize( )
When
you use composition to create a new class, you never worry about finalizing the
member objects of that class. Each member is an independent object and thus is garbage
collected and finalized regardless of whether it happens to be a member of your
class. With inheritance, however, you must override finalize( )
in the derived class if you have any special cleanup that must happen as part
of garbage collection. When you override
finalize( )
in an inherited class, it’s important to remember to call the base-class
version of
finalize( ),
since otherwise the base-class finalization will not happen. The following
example proves this:
//: Frog.java
// Testing finalize with inheritance
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println(
"Creating Characteristic " + s);
}
protected void finalize() {
System.out.println(
"finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p =
new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println(
"LivingCreature finalize");
// Call base-class version LAST!
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Animal extends LivingCreature {
Characteristic p =
new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Amphibian extends Animal {
Characteristic p =
new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
public static void main(String[] args) {
if(args.length != 0 &&
args[0].equals("finalize"))
DoBaseFinalization.flag = true;
else
System.out.println("not finalizing bases");
new Frog(); // Instantly becomes garbage
System.out.println("bye!");
// Must do this to guarantee that all
// finalizers will be called:
System.runFinalizersOnExit(true);
}
} ///:~
The
class
DoBaseFinalization
simply holds a flag that indicates to each class in the hierarchy whether to
call super.finalize( ).
This flag is set based on a command-line argument, so you can view the behavior
with and without base-class finalization.
Each
class in the hierarchy also contains a member object of class
Characteristic.
You will see that regardless of whether the base class finalizers are called,
the
Characteristic
member objects are always finalized.
Each
overridden
finalize( )
must
have access to at least
protected
members
since the
finalize( )
method
in class
Object
is
protected
and the compiler will not allow you to reduce the access during inheritance.
(“Friendly” is less accessible than
protected.)
In
Frog.main( ),
the
DoBaseFinalization
flag
is configured and a single
Frog
object
is created. Remember that garbage collection and in particular finalization
might not happen for any particular object so to enforce this,
System.runFinalizersOnExit(true)
adds the extra overhead to guarantee that finalization takes place. Without
base-class finalization, the output is:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
You
can see that, indeed, no finalizers are called for the base classes of
Frog.
But if you add the “finalize” argument on the command line, you get:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Although
the order the member objects are finalized is the same order that they are
created, technically the order
of finalization of objects is unspecified. With base classes, however, you have
control over the order of finalization. The best order to use is the one
that’s shown here, which is the reverse of the order of initialization.
Following the form that’s used in C++ for destructors, you should perform
the derived-class finalization first, then the base-class finalization.
That’s because the derived-class finalization could call some methods in
the base class that require that the base-class components are still alive, so
you must not destroy them prematurely.
Behavior
of polymorphic methods
inside
constructors
The
hierarchy of constructor calls brings up an interesting dilemma. What happens
if you’re inside a constructor and you call a dynamically-bound method of
the object being constructed? Inside an ordinary method you can imagine what
will happen – the dynamically-bound call is resolved at run-time because
the object cannot know whether it belongs to the class the method is in or some
class derived from it. For consistency, you might think this is what should
happen inside constructors.
This
is not exactly the case. If you call a dynamically-bound method inside a
constructor, the overridden definition for that method is used. However, the
effect
can be rather unexpected, and can conceal some difficult-to-find bugs.
Conceptually,
the constructor’s job is to bring the object into existence (which is
hardly an ordinary feat). Inside any constructor, the entire object might be
only partially formed – you can know only that the base-class objects
have been initialized, but you cannot know which classes are inherited from
you. A dynamically-bound method call, however, reaches “forward” or
“outward” into the inheritance hierarchy. It calls a method in a
derived class. If you do this inside a constructor, you call a method that
might manipulate members that haven’t been initialized yet – a sure
recipe for disaster.
You
can see the problem in the following example:
//: PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = "
+ radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} ///:~
In
Glyph,
the
draw( )
method is
abstract,
so it is designed to be overridden. Indeed, you are forced to override it in
RoundGlyph.
But the
Glyph
constructor calls this method, and the call ends up in
RoundGlyph.draw( ),
which would seem to be the intent. But look at the output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
When
Glyph’s
constructor calls
draw( ),
the value of
radius
isn’t even the default initial value 1. It’s zero. This would
probably result in either a dot or nothing at all being drawn on the screen,
and you’d be staring, trying to figure out why the program won’t
work.
The
order
of initialization described in the previous section isn’t quite complete,
and that’s the key to solving the mystery. The actual process of
initialization is:
- The
storage allocated for the object is initialized to binary zero before anything
else happens.
- The
base-class constructors are called as described previously. At this point, the
overridden
draw( )
method is called, (yes,
before
the
RoundGlyph
constructor
is called), which discovers a
radius
value of zero, due to step 1.
- Member
initializers are called in the order of declaration.
- The
body of the derived-class constructor is called.
There’s
an upside to this, which is that everything is at least initialized to zero (or
whatever zero means for that particular data type) and not just left as
garbage. This includes object handles that are embedded inside a class via
composition. So if you forget to initialize that handle you’ll get an
exception at run time. Everything else gets zero, which is usually a telltale
value when looking at output.
On
the other hand, you should be pretty horrified at the outcome of this program.
You’ve done a perfectly logical thing and yet the behavior is
mysteriously wrong, with no complaints from the compiler. (C++ produces more
rational behavior in this situation.) Bugs like this could easily be buried and
take a long time to discover.
As
a result, a good guideline for constructors is, “Do as little as possible
to set the object into a good state, and if you can possibly avoid it,
don’t call any methods.” The only safe methods to call inside a
constructor are those that are
final
in the base class. (This also applies to
private
methods, which are automatically
final.)
These cannot be overridden and thus cannot produce this kind of surprise.