|
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 explicitly write a special method to do it, and make sure that the client programmer knows that they must call this method. On top of this—as described in Chapter 9 (“Error Handling with Exceptions”)—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:
//: c06:CADSystem.java
// Ensuring proper cleanup.
package c06;
import com.bruceeckel.simpletest.*;
import java.util.*;
class Shape {
Shape(int i) {
System.out.println("Shape constructor");
}
void dispose() {
System.out.println("Shape dispose");
}
}
class Circle extends Shape {
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
void dispose() {
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape {
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
void dispose() {
System.out.println("Erasing Triangle");
super.dispose();
}
}
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 Line: "+ start+ ", "+ end);
}
void dispose() {
System.out.println("Erasing Line: "+ start+ ", "+ end);
super.dispose();
}
}
public class CADSystem extends Shape {
private static Test monitor = new Test();
private Circle c;
private Triangle t;
private Line[] lines = new Line[5];
public CADSystem(int i) {
super(i + 1);
for(int j = 0; j < lines.length; j++)
lines[j] = new Line(j, j*j);
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
public void dispose() {
System.out.println("CADSystem.dispose()");
// The order of cleanup is the reverse
// of the order of initialization
t.dispose();
c.dispose();
for(int i = lines.length - 1; i >= 0; i--)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try {
// Code and exception handling...
} finally {
x.dispose();
}
monitor.expect(new String[] {
"Shape constructor",
"Shape constructor",
"Drawing Line: 0, 0",
"Shape constructor",
"Drawing Line: 1, 1",
"Shape constructor",
"Drawing Line: 2, 4",
"Shape constructor",
"Drawing Line: 3, 9",
"Shape constructor",
"Drawing Line: 4, 16",
"Shape constructor",
"Drawing Circle",
"Shape constructor",
"Drawing Triangle",
"Combined constructor",
"CADSystem.dispose()",
"Erasing Triangle",
"Shape dispose",
"Erasing Circle",
"Shape dispose",
"Erasing Line: 4, 16",
"Shape dispose",
"Erasing Line: 3, 9",
"Shape dispose",
"Erasing Line: 2, 4",
"Shape dispose",
"Erasing Line: 1, 1",
"Shape dispose",
"Erasing Line: 0, 0",
"Shape dispose",
"Shape dispose"
});
}
} ///:~
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 overrides Shape’s dispose( ) 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 dispose( ) method to restore nonmemory 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 nonordinary ways.) Here, the finally clause is saying “always call dispose( ) 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 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 are required, because 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( ).
|
|