Back to JVM Internals: Advanced
Q8Question 8 of 10

Why is finalize() deprecated and what replaces it?

Key Concepts
  • finalize() is deprecated since Java 9 because it is unpredictable, slow, and error-prone
  • Not guaranteed to run – the GC may never call it
  • Objects with finalizers take at least 2 GC cycles to collect (finalization queue)
  • Replacement: try-with-resources (AutoCloseable) for deterministic cleanup
  • Replacement: Cleaner API (Java 9+) for native resource cleanup using phantom references

Definition

finalize() is deprecated because it is unpredictable, delays GC, and causes security issues. Use try-with-resources for deterministic cleanup and Cleaner for native resources.

Why finalize is bad:

  • Not guaranteed. The JVM may never call it.
  • Unpredictable timing. You cannot control when it runs.
  • Delays GC. Objects with finalizers need 2+ GC cycles (finalizer queue processing).
  • Security risk. Malicious subclasses can resurrect objects in finalize.
  • Deprecated since Java 9. Slated for removal.

finalize() is like a security guard who sometimes checks the vault after closing but sometimes forgets, sometimes arrives the next day, and sometimes lets intruders in. Replace with an automatic vault lock (try-with-resources) that always works.

Understanding the Concepts

The old way (do NOT use)

JAVA
@Override
protected void finalize() throws Throwable {
    connection.close();  // may never run!
    super.finalize();
}

The correct way: try-with-resources

JAVA
try (Connection conn = dataSource.getConnection()) {
    // use connection
}  // automatically closed here, ALWAYS

The Cleaner API (Java 9+)

JAVA
public class NativeResource implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final long nativePtr;

    public NativeResource() {
        this.nativePtr = allocateNative();
        this.cleanable = cleaner.register(this, () -> freeNative(nativePtr));
    }

    @Override
    public void close() {
        cleanable.clean();  // deterministic cleanup
    }
    // If close() is not called, the Cleaner's phantom reference triggers cleanup eventually
}

What should a senior engineer know?

  • Cleaner uses phantom references internally. It is the modern, safe replacement for finalize.
  • AutoCloseable + try-with-resources is the primary cleanup pattern for 99% of cases.
  • Cleaner is the fallback for native resources when the developer forgets to call close().

What are common mistakes?

A common mistake is not understanding the concept fully before using it in production banking applications.

Wrong approach:

JAVA
package com.bank.service;

public class TransferService {
    public void transfer(String fromAccount, String toAccount, double amount) {
        // WRONG: using double for money
        double balance = getBalance(fromAccount);
        balance = balance - amount;
    }
    private double getBalance(String id) { return 10000.0; }
}

Correct approach:

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class TransferService {
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // CORRECT: using BigDecimal for money
        BigDecimal balance = getBalance(fromAccount);
        balance = balance.subtract(amount);
    }
    private BigDecimal getBalance(String id) { return new BigDecimal("10000.00"); }
}

Always use BigDecimal for monetary calculations in banking systems to avoid floating-point precision errors.

What are the edge cases?

Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.

JAVA
package com.bank.edge;

public class EdgeCaseDemo {
    public static void main(String[] args) {
        // Edge case 1: null handling
        String accountId = null;
        try {
            System.out.println(accountId.length());
        } catch (NullPointerException e) {
            System.out.println("Null account ID");
        }

        // Edge case 2: empty vs null
        String emptyId = "";
        System.out.println(emptyId.isEmpty());  // true
        System.out.println(emptyId == null);     // false

        // Edge case 3: boundary values
        java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
        java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
        System.out.println(zero.compareTo(negativeBalance) > 0);  // true
    }
}
TEXT
Null account ID
true
false
true

Always test with null values, empty strings, boundary values, and negative amounts in banking applications.

What are common interview follow-up questions?

Interviewers often follow up with these related questions:

  • How does this concept apply to a high-throughput banking transaction system?
  • What are the performance implications in a production environment?
  • How would you debug issues related to this concept?
  • What tools would you use to monitor this in production?
  • How does this behave differently in Java 8 vs Java 17+?

When answering follow-up questions, always relate your answer back to practical experience. For example, at National Bank of India, Rajesh debugged a production issue related to this concept by using JFR recordings and heap dumps. Providing concrete examples from banking systems shows the interviewer that you understand both the theory and the practice.

Visual Overview

DIAGRAM
  finalize() (deprecated):
    Not guaranteed, unpredictable, delays GC, security risk

  Replacements:
    1. try-with-resources → deterministic, always runs
    2. Cleaner API (Java 9+) → phantom refs, safe fallback
    3. Both → close() for deterministic, Cleaner as safety net

Key Takeaway

finalize() is deprecated because it is unpredictable, delays GC, and has security issues. Use try-with-resources for deterministic cleanup and the Cleaner API for native resources as a safety net.

One sentence to remember: finalize is dead – use try-with-resources for cleanup and Cleaner for native resource safety nets.

---

Share
Use ← → arrow keys to navigate