Skip to content
Part IA Michaelmas Term

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 (br before fr).
  • Resources are closed even if an exception occurs in the try body.
  • Resources must implement AutoCloseable (the modern interface) or the older Closeable (which extends AutoCloseable).
  • 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++

PropertyC++ (RAII)Java
Destruction timingDeterministic — when object goes out of scopeNon-deterministic — GC decides
MechanismDestructor ~T()AutoCloseable.close() via try-with-resources
For stack-allocated objectsAutomaticNot applicable (all objects are heap-allocated)
For heap objectsdelete must be called (or smart pointers manage it)GC handles memory; resources need manual management
GuaranteeDestructor always runs for stack objectsclose() 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.