final, finally and finalize look alike but are three different things: final is a keyword that stops a variable being reassigned, a method being overridden or a class being extended; finally is a block that runs after try/catch so cleanup code executes; finalize() is a method the garbage collector could call before reclaiming an object, and it is now deprecated for removal.
finally edge cases and the current status of finalize() on JDK 25.
Versions. Every program on this page was compiled and run on JDK 25.0.4.1 (Temurin, LTS, build 25.0.4.1+1-LTS). Where Java 8 behaved differently the text says so. The compiler errors and console output are quoted verbatim from those runs; the “Download” links at the end of each section point to the exact .java files.
final vs finally vs finalize: the table
| final | finally | finalize() | |
|---|---|---|---|
| What it is | Keyword (modifier) | Block in try-catch-finally | Method of java.lang.Object |
| Applies to | Variables, methods, classes, parameters | Code after try/catch | Objects about to be garbage collected |
| Purpose | Prevent change, override or inheritance | Run cleanup whether or not an exception happened | Last-chance resource cleanup (legacy) |
| When it takes effect | Compile time | Run time, at the end of the try statement | Run time, at an unpredictable moment, if at all |
| Status in Java 25 | Current | Current | Deprecated for removal ([removal] warning) |
| Better alternative | — | try-with-resources for closeable resources | AutoCloseable or java.lang.ref.Cleaner |
final: variables, methods and classes
- final variable: can be assigned once. For a reference, the reference is fixed, not the object it points to.
- final method: subclasses cannot override it.
- final class: cannot be extended (
Stringand the wrapper classes are final).
final int[] data = { 1, 2 };
data[0] = 100; // the object is mutable; only the reference is final
System.out.println("data[0] = " + data[0]);
$ java FinalFinally.java
-- final variable, final reference --
data[0] = 100
data is final, yet data[0] changed to 100. The array object is mutable; only the variable data cannot point somewhere else. For true immutability you need an immutable type (see Immutable Collections in Java).
Breaking each rule is a compile error. BrokenFinal.java:
public class BrokenFinal {
static final class Leaf { }
static class Sub extends Leaf { }
static class P { final void m() { } }
static class C extends P { void m() { } }
final int x;
BrokenFinal() { }
public static void main(String[] args) {
final int y = 1;
y = 2;
final java.util.List<String> l = new java.util.ArrayList<>();
l = null;
}
}
$ javac BrokenFinal.java
BrokenFinal.java:3: error: cannot inherit from final Leaf
static class Sub extends Leaf { }
^
BrokenFinal.java:5: error: m() in C cannot override m() in P
static class C extends P { void m() { } }
^
overridden method is final
BrokenFinal.java:10: error: cannot assign a value to final variable y
y = 2;
^
BrokenFinal.java:12: error: cannot assign a value to final variable l
l = null;
^
4 errors
All four final rules are rejected at compile time: extending a final class, overriding a final method, reassigning a final local, and reassigning a final reference. The field x in that file is final and never assigned, yet it is not in the list, because the compiler reports that kind of error only after the others are fixed. In isolation:
public class BrokenFinalField {
final int x;
BrokenFinalField() { }
public static void main(String[] args) { }
}
$ javac BrokenFinalField.java
BrokenFinalField.java:3: error: variable x might not have been initialized
BrokenFinalField() { }
^
1 error
A final field must be assigned exactly once by the end of every constructor, which is why final fields are the foundation of immutable objects (and why Constructors in Java matters here).
finally: it runs, with four surprises
A finally block runs whether the try completed normally, returned, or threw. That is its job, and it is why it was the standard place to close files and connections. Four behaviours surprise people, all from FinalFinally.java:
@SuppressWarnings("finally")
static int returnInFinally() {
try {
return 1;
} finally {
return 2; // overrides the try's return
}
}
static int finallyCannotChangePrimitive() {
int x = 1;
try {
return x; // value is captured here
} finally {
x = 99;
}
}
@SuppressWarnings("finally")
static void finallySwallowsException() {
try {
throw new IllegalStateException("original problem");
} finally {
return; // the exception disappears
}
}
$ java FinalFinally.java
-- finally edge cases --
return in finally -> 2
primitive changed in finally -> 1
exception swallowed, no stack trace printed
returninfinallywins. The method returned2, not1, because thefinallyreturn replaced thetryreturn.- The return value is captured early.
finallyCannotChangePrimitivereturned1even thoughfinallysetx = 99: the value was copied whenreturn xexecuted. (If the value were a mutable object, changes to the object would be visible.) - A
returninfinallyswallows exceptions. TheIllegalStateExceptionvanished: no stack trace, no failure. Never return fromfinally. - It does not run if the JVM stops. See the last block below.
Why the@SuppressWarnings("finally"). The compiler warns aboutreturninsidefinally(with-Xlint:finally) because it is almost always a bug. It is suppressed here only so the demo compiles quietly.
System.exit skips finally
System.out.println("-- System.exit skips finally --");
try {
System.exit(0);
} finally {
System.out.println("this line never prints");
}
$ java FinalFinally.java
-- System.exit skips finally --
exit=0
The line “this line never prints” indeed did not print. (exit=0 is the shell’s exit status, echoed after the program ended; the program itself printed nothing after the header.) A crash, kill -9 or power loss skips finally as well, so do not rely on it for correctness of data that must survive.
Prefer try-with-resources
Closing resources by hand infinally is verbose and buggy (an exception in close() can hide the original one). Any AutoCloseable in a try-with-resources header is closed for you, in reverse order:
static class Res implements AutoCloseable {
final String name;
Res(String n) { name = n; System.out.println(" open " + n); }
@Override public void close() { System.out.println(" close " + name); }
}
try (Res a = new Res("A"); Res b = new Res("B")) {
System.out.println(" body");
}
$ java FinalFinally.java
-- try-with-resources closes in reverse order --
open A
open B
body
close B
close A
B was opened second and closed first. Closing happens even when the body throws, and an exception from close() is attached as a suppressed exception rather than replacing the original, which is the failure that hand-written finally blocks get wrong.
finalize(): deprecated, unreliable, and you should not use it
finalize() is a protected method on Object. The idea was that the garbage collector would call it once, before reclaiming the object, so you could free native resources. In practice it ran at unpredictable times (or never), could resurrect objects, slowed down collection, and was a source of security bugs. It has been deprecated since Java 9 and marked for removal since Java 18. Here is what JDK 25 does with FinalizeDemo.java:
public class FinalizeDemo {
@Override
protected void finalize() {
System.out.println("finalize ran");
}
public static void main(String[] args) throws Exception {
new FinalizeDemo();
System.gc();
Thread.sleep(500);
System.out.println("main done");
}
}
$ javac FinalizeDemo.java
FinalizeDemo.java:3: warning: [removal] finalize() in Object has been deprecated and marked for removal
protected void finalize() {
^
1 warning
The compiler now warns on every override. It still runs by default on JDK 25:
$ java FinalizeDemo
finalize ran
main done
…and a command-line switch that turns finalization off completely shows what “removal” will look like:
$ java --finalization=disabled FinalizeDemo
main done
With --finalization=disabled the same program never prints finalize ran. Any code that depended on finalizers silently stops cleaning up, which is the reason to migrate now rather than at removal.
Even the “works” run proves little. The finalizer ran only because the program called System.gc() and then slept for half a second. A real application gets no such guarantee: the JVM may exit before the finalizer runs, and there is no promised timing.
What to use instead
- try-with-resources with
AutoCloseablefor anything you open and close in a scope (files, sockets, connections). java.lang.ref.Cleaneras a safety net for native resources owned by long-lived objects. A cleanup action registered on it runs at most once, and you can trigger it explicitly:
System.out.println("-- Cleaner: explicit, registered, not tied to GC timing for correctness --");
Cleaner cleaner = Cleaner.create();
Object owner = new Object();
Cleaner.Cleanable c = cleaner.register(owner, new Tracked());
c.clean(); // deterministic; calling twice is harmless
c.clean();
$ java FinalFinally.java
-- Cleaner: explicit, registered, not tied to GC timing for correctness --
cleaner action ran
clean() was called twice and the action ran once. A Cleaner action must not reference the object it cleans (or the object can never become unreachable), which is why the demo registers a separate Tracked object.
Going deeper
- Closing resources and exception handling in concurrent code: Top 40 Java Concurrency Interview Questions
- Immutable types, which make
finalmeaningful: Immutable Collections in Java - Where final fields get their value: Constructors in Java
- final and static together for constants: static Keyword in Java
- JEP 421, deprecating finalization for removal: openjdk.org/jeps/421
- API reference for the replacement: java.lang.ref.Cleaner
Download the code. FinalFinally.java · FinalizeDemo.java · BrokenFinal.java.
Run a runnable one withjava File.java(JDK 11 or later; JDK 25 recommended). Files starting withBrokenare meant to fail to compile.
Common interview questions
1. What is the difference between final, finally and finalize?
final is a modifier for variables, methods and classes; finally is a block that runs after try/catch; finalize() is a deprecated method the GC could call before reclaiming an object.
2. Can a final variable be modified?
A final variable cannot be reassigned, but if it refers to a mutable object, that object can still change (the data[0] = 100 example above).
3. Can we override a final method? Extend a final class?
No to both; the compiler rejects each (shown in the errors above).
4. Is it mandatory to initialise a final variable at declaration?
No, but it must be definitely assigned exactly once: at declaration, in an initialiser block, or (for fields) in every constructor.
5. Does the finally block always execute?
Almost always. It does not run if the JVM exits (System.exit, crash, kill -9) or the thread is stopped forever.
6. What happens if both try and finally contain a return?
The value from finally is returned (2 in the example above), and the try’s return is discarded.
7. Can finally change the value being returned?
Not for a primitive or reference already captured by return; the earlier value is returned (1 in the example). Changes to a mutable object are visible.
8. What if both try and finally throw an exception?
The exception from finally replaces the one from try, which is lost. try-with-resources avoids this by recording the second as a suppressed exception.
9. Can you have try without catch?
Yes, with a finally block or with resources (try-with-resources).
10. Is finalize() guaranteed to run?
No. It may run late or never, and it is deprecated for removal. On JDK 25 it still runs by default, and --finalization=disabled turns it off.
11. What replaced finalize()?
try-with-resources for scoped resources and java.lang.ref.Cleaner as a safety net.
12. Is a final class the same as an immutable class?
No. Final prevents subclassing; immutability also needs private final fields, no mutators and defensive copies.
13. Can a constructor or an abstract method be final?
No. Constructors are not inherited, and abstract and final contradict each other.
Common mistakes
- Assuming a final reference makes the object immutable. Only the variable is fixed.
returnorthrowinsidefinally. It overrides the original result and can hide exceptions.- Closing resources by hand in
finally. Use try-with-resources. - Relying on
finalize()for cleanup. It is unpredictable, deprecated for removal, and can be disabled. - Expecting
finallyto run afterSystem.exit. It does not. - Making classes final “for safety” while keeping mutable public fields. That is not immutability.
FAQ
What is the difference between final, finally and finalize in Java?
final is a keyword that prevents reassigning a variable, overriding a method or extending a class. finally is a block that runs after try and catch for cleanup. finalize is a deprecated Object method the garbage collector could call before reclaiming an object.
Does finally always run in Java?
Nearly always. It runs after the try block completes normally, returns or throws, but not if the JVM exits through System.exit, crashes or is killed.
Is finalize deprecated in Java?
Yes. It has been deprecated since Java 9 and marked for removal since Java 18. Java 25 still runs it by default, warns when you override it, and can disable it with –finalization=disabled.
What should I use instead of finalize in Java?
Use try-with-resources with AutoCloseable for scoped resources, and java.lang.ref.Cleaner as a safety net for native resources owned by long-lived objects.
Can a final variable be changed in Java?
A final variable cannot be reassigned, but the object it refers to can still be mutated if the object is mutable, as with a final array or list.
Where to go next
- OOPs Concepts in Java (encapsulation and immutability in context)
- Constructors in Java (initialising final fields)
- Top 40 Java Concurrency Interview Questions (exception handling, executors and resource cleanup)
- Top 40 Java Collections Interview Questions (immutable and unmodifiable collections)
- Top 50 Java Stream API Coding Interview Questions (resources and streams that must be closed)
- Immutable Collections in Java
No Comments yet!