Destructors and finalize()
Java has no deterministic destructor — unlike C++‘s RAII (Resource Acquisition Is Initialisation), where a destructor runs the moment an object goes out of scope.
The finalize() Anti-Pattern
Object.finalize() was intended as a cleanup hook that the garbage collector would call before reclaiming an object. It is deprecated in Java 9+ and removed in Java 18. The problems are fundamental:
1. Unpredictable Timing
The GC may never run, or may run long after the resource is no longer needed. A file handle held open until finalize() could keep the file locked indefinitely. There is no guarantee when or even if finalize() will execute.
class BadResource {
@Override
protected void finalize() throws Throwable {
// This might never run. Or it might run after the file is already stale.
closeFile();
}
}
2. Object Resurrection
Inside finalize(), an object can re-store its this reference into a static field or collection, making itself reachable again:
class Zombie {
static Zombie resurrected;
@Override
protected void finalize() {
resurrected = this; // the object comes back to life
}
}
The JVM detects resurrection and declines to finalise the object a second time — but the object evades collection until it becomes unreachable again. This undermines the guarantee that GC reclaims all garbage eventually.
3. Silent Exception Swallowing
If finalize() throws an exception, the JVM silently swallows it — no stack trace, no catch block, no indication that anything went wrong. The finalisation process for that object is simply terminated, and the bit of cleanup that was supposed to happen does not.
The Correct Approach: Try-With-Resources and AutoCloseable
For deterministic cleanup of resources (file handles, network connections, database resources, locks), Java provides the try-with-resources construct:
try (FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr)) {
String line = br.readLine();
System.out.println(line);
} // fr.close() and br.close() called automatically here
Key properties:
- Resources are closed in reverse order of declaration (
brbeforefr). - Resources are closed even if an exception occurs in the try body.
- Resources must implement
AutoCloseable(the modern interface) or the olderCloseable(which extendsAutoCloseable). - The close methods are called in a finally-like block after the try body completes normally or exceptionally.
The AutoCloseable Interface
public interface AutoCloseable {
void close() throws Exception;
}
A typical implementation:
class ManagedConnection implements AutoCloseable {
private boolean open = true;
void send(String data) {
if (!open) throw new IllegalStateException("Connection closed");
}
@Override
public void close() {
open = false;
// release the real resource
}
}
Suppressed Exceptions
If both the try body and a close() call throw exceptions, the close exception is suppressed and attached to the try body’s exception. You can retrieve suppressed exceptions via Throwable.getSuppressed(). This ensures that a secondary failure during cleanup does not hide the primary failure.
Multiple Resources
try (FileInputStream fis = new FileInputStream("in.bin");
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis)) {
int value = dis.readInt();
} // dis, bis, fis closed in that order
Contrast with C++
| Property | C++ (RAII) | Java |
|---|---|---|
| Destruction timing | Deterministic — when object goes out of scope | Non-deterministic — GC decides |
| Mechanism | Destructor ~T() | AutoCloseable.close() via try-with-resources |
| For stack-allocated objects | Automatic | Not applicable (all objects are heap-allocated) |
| For heap objects | delete must be called (or smart pointers manage it) | GC handles memory; resources need manual management |
| Guarantee | Destructor always runs for stack objects | close() runs only if you use try-with-resources |
C++‘s approach is more powerful for resource management — you can wrap any resource in a class whose destructor releases it, and the language guarantees the destructor runs. Java requires discipline: you must use try-with-resources or manually call close() in a finally block. The trade-off is that Java eliminates the entire class of bugs around memory management (double-free, use-after-free, dangling pointers) that C++ must handle, at the cost of less deterministic resource cleanup.