Combining composition
Posted
on March 1st, 2001
Bruce Eckel's Thinking in Java | Contents | Prev | Next |
and inheritance
It
is very common to use composition and inheritance together. The following
example shows the creation of a more complex class, using both inheritance and
composition, along with the necessary constructor
initialization:
//: PlaceSetting.java // Combining composition & inheritance class Plate { Plate(int i) { System.out.println("Plate constructor"); } } class DinnerPlate extends Plate { DinnerPlate(int i) { super(i); System.out.println( "DinnerPlate constructor"); } } class Utensil { Utensil(int i) { System.out.println("Utensil constructor"); } } class Spoon extends Utensil { Spoon(int i) { super(i); System.out.println("Spoon constructor"); } } class Fork extends Utensil { Fork(int i) { super(i); System.out.println("Fork constructor"); } } class Knife extends Utensil { Knife(int i) { super(i); System.out.println("Knife constructor"); } } // A cultural way of doing something: class Custom { Custom(int i) { System.out.println("Custom constructor"); } } public class PlaceSetting extends Custom { Spoon sp; Fork frk; Knife kn; DinnerPlate pl; PlaceSetting(int i) { super(i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3); kn = new Knife(i + 4); pl = new DinnerPlate(i + 5); System.out.println( "PlaceSetting constructor"); } public static void main(String[] args) { PlaceSetting x = new PlaceSetting(9); } } ///:~
While
the compiler forces you to initialize the base classes, and requires that you
do it right at the beginning of the constructor, it doesn’t watch over
you to make sure that you initialize the member objects, so you must remember
to pay attention to that.
Guaranteeing proper cleanup
Java
doesn’t have the C++ concept of a destructor,
a method that is automatically called when an object is destroyed. The reason
is probably that in Java the practice is simply to forget about objects rather
than to destroy them, allowing the garbage
collector to reclaim the memory as necessary.
Often
this is fine, but there are times when your class might perform some activities
during its lifetime that require cleanup. As mentioned in Chapter 4, you
can’t know when the garbage collector will be called, or if it will be
called. So if you want something cleaned up for a class, you must write a
special method to do it explicitly, and make sure that the client programmer
knows that they must call this method. On top of this, as described in Chapter
9 (exception
handling), you must guard against an exception by putting such cleanup in a finally
clause.
Consider
an example of a computer-aided design system that draws pictures on the screen:
//: CADSystem.java // Ensuring proper cleanup import java.util.*; class Shape { Shape(int i) { System.out.println("Shape constructor"); } void cleanup() { System.out.println("Shape cleanup"); } } 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(); } } 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()"); t.cleanup(); c.cleanup(); for(int i = 0; i < lines.length; 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 non-memory things back to the way they were before the object
existed.
In
main( ),
you can see two keywords that are new, and won’t officially be introduced
until Chapter 9: 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 non-ordinary ways.) Here, the
finally
clause is saying “always call
cleanup( )
for
x,
no matter what happens.” These keywords will be explained thoroughly in
Chapter 9.
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 work specific to
your class (which might require 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. In addition, implementations of the
garbage collector in Java 1.0
often don’t call the finalize( )
methods. 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 earlier, Java 1.1
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:
//: 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()); } } ///:~