Skip to content
Java exceptions 6 min read

final vs finally vs finalize

Three Java words — final, finally, and finalize — look nearly identical but serve completely different purposes. Beginners mix them up constantly; even experienced developers occasionally pause to think through which one to reach for. This page clears up all three once and for all.

Quick Comparison

Featurefinalfinallyfinalize
TypeKeywordKeyword (block)Method
Used withVariables, methods, classestry-catch blocksObject class
PurposePrevent modification/overrideGuarantee execution after tryPre-GC cleanup hook (deprecated)
Belongs toLanguage syntaxException handlingjava.lang.Object
Always runs?N/AYes (almost)Not guaranteed

final — Locking Things In Place

The final keyword is a modifier you place on variables, methods, or classes to prevent them from being changed or extended.

final Variables

A final variable can only be assigned once. After that, any attempt to reassign it causes a compile-time error.

public class CircleCalculator {
    public static void main(String[] args) {
        final double PI = 3.14159;
        double radius = 5.0;
        System.out.println("Area: " + (PI * radius * radius));

        // PI = 3.14; // Compile error: cannot assign a value to final variable PI
    }
}

Output:

Area: 78.53975

Tip: By convention, final constants are written in UPPER_SNAKE_CASE.

final Methods

A final method cannot be overridden in a subclass. Use it when a method’s behavior must stay consistent across all subclasses.

class Vehicle {
    public final void startEngine() {
        System.out.println("Engine started.");
    }
}

class Car extends Vehicle {
    // public void startEngine() { } // Compile error: cannot override final method
}

final Classes

A final class cannot be subclassed at all. java.lang.String is the most famous example — this guarantees its immutability and thread safety.

final class ImmutablePoint {
    private final int x, y;

    ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }
}

// class SpecialPoint extends ImmutablePoint { } // Compile error

Note: Making a class final also allows the JIT compiler to inline method calls, which can improve performance since no virtual dispatch is needed.


finally — The Cleanup Block That Always Runs

The finally block belongs to Java’s exception handling mechanism. It is attached to a try-catch statement and always executes after the try block finishes — whether an exception was thrown or not.

This makes it perfect for releasing resources: closing files, database connections, or network sockets.

public class FileDemo {
    public static void main(String[] args) {
        try {
            System.out.println("Opening file...");
            int result = 10 / 0; // ArithmeticException thrown here
            System.out.println("This won't print.");
        } catch (ArithmeticException e) {
            System.out.println("Caught: " + e.getMessage());
        } finally {
            System.out.println("Finally block runs — always!");
        }
    }
}

Output:

Opening file...
Caught: / by zero
Finally block runs — always!

finally Without catch

You can use finally without a catch block. This is useful when you want to guarantee cleanup but still let any exception propagate up to the caller.

public static void riskyOperation() {
    try {
        System.out.println("Doing risky work...");
        // exception might bubble up
    } finally {
        System.out.println("Cleanup always happens.");
    }
}

When Does finally NOT Run?

There is one rare case: if the JVM exits inside the try or catch block via System.exit(), the finally block is skipped entirely. An abrupt JVM crash (like running out of memory at the OS level) can also prevent it.

try {
    System.out.println("Before exit");
    System.exit(0); // JVM terminates — finally is NOT called
} finally {
    System.out.println("This will NOT print.");
}

Warning: Do not use System.exit() inside library code. It kills the entire JVM and bypasses all cleanup blocks and shutdown hooks.

try-with-resources: The Modern Alternative

Since Java 7, try-with-resources automatically closes any AutoCloseable resource at the end of the block. For resource cleanup, this is cleaner than a manual finally.

try (var reader = new java.io.BufferedReader(new java.io.FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} catch (java.io.IOException e) {
    System.out.println("Error: " + e.getMessage());
}
// reader.close() is called automatically — no finally needed

Tip: Prefer try-with-resources over manual finally for Closeable/AutoCloseable objects. It handles suppressed exceptions correctly and is harder to get wrong.


finalize — Pre-Garbage-Collection Hook (Deprecated)

finalize() is an instance method inherited from java.lang.Object. The garbage collector was supposed to call it just before reclaiming an unreachable object, giving you a chance to free native resources.

In practice, finalize() is unreliable and was deprecated in Java 9 and marked for removal. You should not use it in new code.

public class ResourceHolder {
    private String name;

    ResourceHolder(String name) {
        this.name = name;
    }

    @Override
    @Deprecated
    protected void finalize() throws Throwable {
        try {
            System.out.println("finalize() called for: " + name);
        } finally {
            super.finalize();
        }
    }

    public static void main(String[] args) throws Exception {
        ResourceHolder obj = new ResourceHolder("myResource");
        obj = null; // eligible for GC
        System.gc(); // suggest GC — no guarantee finalize() will run now
        Thread.sleep(100); // give GC a chance (not reliable in real code!)
        System.out.println("Main done.");
    }
}

Output (not guaranteed):

finalize() called for: myResource
Main done.

Warning: finalize() has serious problems: it may run on any thread, may never run at all, can cause objects to “resurrect” themselves (preventing collection), and adds significant GC overhead. The Java team officially deprecated it in Java 9 and marked it for removal.

What to Use Instead

Old approachModern replacement
finalize() for resource cleanupImplement AutoCloseable + try-with-resources
finalize() for native memoryUse java.lang.ref.Cleaner (Java 9+)
finalize() for post-GC logicPhantomReference + ReferenceQueue

Here is the clean, modern approach using AutoCloseable:

public class NativeResource implements AutoCloseable {
    public NativeResource() {
        System.out.println("Resource acquired.");
    }

    @Override
    public void close() {
        System.out.println("Resource released cleanly.");
    }

    public static void main(String[] args) {
        try (NativeResource r = new NativeResource()) {
            System.out.println("Using resource...");
        } // close() is called automatically here
    }
}

Output:

Resource acquired.
Using resource...
Resource released cleanly.

Under the Hood

final variables and the JIT. When the compiler sees a final field that is initialized in the constructor and never reassigned, it can treat it as a constant and fold its value into other expressions at compile time. For static final primitives and String literals, javac inlines the value directly into the bytecode of every class that references it — the field lookup disappears entirely.

finally and bytecode. The Java compiler does not generate a special bytecode for finally. Instead, it duplicates the finally block into every exit path of the try: the normal return path, each catch path, and the re-throw path. You can confirm this with the javap tool — you will see the same cleanup instructions appear several times in the bytecode. This also explains why returning a value inside finally overrides a value returned inside try — the finally return executes last.

finalize and GC overhead. When an object overrides finalize(), the JVM must place it on a special finalizer queue instead of reclaiming it immediately. A dedicated finalizer thread processes that queue and calls finalize(). Only after finalize() returns can the GC reclaim the memory — on the next GC cycle. This two-pass overhead is expensive and unpredictable, which is why modern Java strongly discourages it.


Summary

  • final — a compile-time constraint. Use it on variables for constants, on methods to prevent overriding, and on classes to prevent subclassing.
  • finally — a runtime guarantee. Use it to run cleanup code after a try-catch block, or better yet, use try-with-resources instead.
  • finalize — a deprecated, unreliable pre-GC hook. Do not use it in new code; implement AutoCloseable instead.
Last updated June 13, 2026
Was this helpful?