skip to content

How does Java's finalize() differ from a C++ destructor, and why doesn't Java have deterministic destructors?

level: middleimportance: nice to knowfreq 30%

answer

  1. C++ destructor = deterministic (scope/delete) -> RAII
  2. Java GC = no defined death point
  3. finalize() = GC-timed pseudo-destructor, deprecated
  4. try-with-resources + AutoCloseable = Java's RAII
  5. Cleaner = non-deterministic native backstop

basics

~20 s

A C++ destructor runs at a predictable moment (when the object goes out of scope or is deleted). Java's finalize() runs whenever the garbage collector decides to, maybe never. Because Java auto-manages memory with a GC, it has no deterministic destructor; you use try-with-resources for predictable cleanup.

solid answer

~50 s

In C++, object lifetime is deterministic: a stack object's destructor runs exactly when it leaves scope, and a heap object's runs when you delete it. This underpins RAII, where acquiring a resource in the constructor and releasing it in the destructor gives automatic, predictable cleanup. Java is different: memory is reclaimed by the garbage collector at an unspecified time, so an object has no well-defined moment of death. finalize() tried to mimic a destructor but is tied to GC, so it offers no timing guarantee, may never run, and runs on a separate thread, at most once. That's why people call it a 'pseudo-destructor.' Java therefore has no deterministic destructor at all. The Java equivalent of RAII is try-with-resources over AutoCloseable: cleanup in close() runs deterministically at the end of the block. For native resources you can add a Cleaner as a non-deterministic safety net, but try-with-resources is the real analogue.

go deeper

for a junior

Knows C++ destructors are predictable and Java's finalize() is not, and that Java uses try-with-resources for cleanup.

for a middle

Explains that Java's GC means no deterministic death point, contrasts the destructor vs finalize() guarantees, and names try-with-resources/AutoCloseable as the RAII analogue.

for a senior

Discusses why a tracing GC precludes per-object deterministic destruction, the full table of differences, and the close()-primary/Cleaner-backstop model for native resources.

for a principal

Reasons about memory-model trade-offs (tracing GC vs reference counting/RAII), the cost of deterministic destruction, and guides API design so resource lifecycles are explicit and finalize()-free.

## Two different memory models To compare the two, start with how each language manages memory. **C++** uses *manual / scope-based* memory management. Object **lifetime** is deterministic: - A **stack object** (a local variable) is destroyed the instant control leaves its scope — its **destructor** (`~T()`) runs right there. - A **heap object** (made with `new`) is destroyed exactly when you call `delete`. Because the destruction *moment* is known, C++ has the **RAII** idiom (*Resource Acquisition Is Initialization*): acquire a resource (file, lock, memory) in the constructor and release it in the destructor. Cleanup then happens automatically and *deterministically* as objects go out of scope — even when exceptions unwind the stack. ```cpp { std::ifstream f("data.txt"); // opens // ... use f } // f's destructor runs HERE, closing the file deterministically ``` **Java** uses *automatic* memory management via a **garbage collector (GC)**. You never `delete`. The GC frees an object's memory at *some unspecified later time* after the object becomes **unreachable**. There is no defined moment of death, so there is **no place to hang a deterministic destructor**. ## What finalize() is — and why it's a poor destructor `finalize()` (inherited from `Object`) was Java's attempt to provide a destructor-like hook: the GC would call it once before reclaiming the object. But because it's bolted onto GC, it inherits all the GC's non-determinism: | Aspect | C++ destructor | Java finalize() | |---|---|---| | When it runs | Deterministic (scope exit / delete) | Whenever the GC gets to it — unspecified | | Guaranteed to run? | Yes | No (may never run, e.g. at JVM exit) | | How many times | Exactly once | At most once (can be skipped after resurrection) | | Thread | The thread doing the destruction | A separate finalizer thread | | Exceptions | Propagate (with care) | Swallowed; abort cleanup | | Status | Core idiom (RAII) | Deprecated (Java 9) / for removal (JEP 421) | So `finalize()` is at best a *pseudo-destructor*: same intent, none of the guarantees. ## Why Java deliberately has no deterministic destructor Deterministic destruction requires knowing precisely when each object dies. A tracing garbage collector intentionally *doesn't* track that per-object — it periodically finds unreachable objects in bulk and frees them, which is what lets it be efficient and relieves the programmer of manual `delete`. Adding deterministic destructors would mean reference counting every object (with cycle problems and overhead) — a different memory model. Java chose GC + an explicit, programmer-driven cleanup idiom instead. ## The Java analogue of RAII: try-with-resources Java's real answer to RAII is **try-with-resources** over the **`AutoCloseable`** interface. You put cleanup in `close()`; the compiler guarantees `close()` runs at the end of the `try` block, in reverse order of acquisition, even on exception (with suppressed exceptions attached). This gives the *deterministic* cleanup C++ gets from destructors — just at a syntactically explicit point rather than implicit scope exit: ```java try (var f = new BufferedReader(new FileReader("data.txt"))) { // ... use f } // f.close() runs HERE, deterministically ``` For resources the GC can't see (**native memory**), you can additionally register a **`Cleaner`** as a *non-deterministic* safety net for callers who forget to close — but it is a backstop, not a destructor. ## Summary - C++ destructors are deterministic (scope/`delete`) → enable RAII. - Java's GC means no deterministic death point → `finalize()` is only a GC-timed pseudo-destructor and is deprecated. - Use **try-with-resources + AutoCloseable** as Java's RAII; add a **Cleaner** only as a backstop for native resources.

  • What is the Java idiom that plays the role of C++ RAII?
    try-with-resources over AutoCloseable: acquire the resource in the try header, and the compiler guarantees its close() runs deterministically at the end of the block (in reverse order, even on exception). It gives RAII-style deterministic cleanup without relying on the GC.

saying these in an interview costs you the question

  • Saying finalize() is Java's deterministic destructor equivalent
  • Claiming Java objects are destroyed when they go out of scope
  • Thinking try-with-resources frees memory (it runs close(); GC frees memory)
  • Believing the GC tracks each object's exact moment of death

context