Final Keyword — Specification Deep-Dive¶
finalrules live in JLS §4.12.4 (final variables), §8.1.1.2 (final classes), §8.4.3.3 (final methods), §8.3.1.2 (final fields), §14.4 (local variable declarations), and most importantly §17.5 (final field semantics in the JMM). Bytecode rules in JVMS §4.5/§4.6 (ACC_FINALflag) and §4.10/§6.5 (verification rules around final fields).
1. Where the rules live¶
| Concept | Source |
|---|---|
| Final variables (locals, parameters, fields) | JLS §4.12.4 |
| Final classes | JLS §8.1.1.2 |
| Final methods | JLS §8.4.3.3 |
| Final fields | JLS §8.3.1.2 |
| Definite assignment (final fields/vars) | JLS §16 |
| Final field semantics in the JMM | JLS §17.5 |
| Compile-time constants | JLS §4.12.4, §13.4.9, §15.28 |
| Sealed and final interaction | JLS §8.1.1.1, §8.1.1.2, §8.1.1.4 |
Class file ACC_FINAL flag | JVMS §4.1, §4.5, §4.6 |
ConstantValue attribute | JVMS §4.7.2 |
| Verifier: final field write rules | JVMS §4.10.1.9, §6.5 (putfield/putstatic) |
2. JLS §4.12.4 — Final variables¶
A final variable is one whose value cannot be changed after it has been initialized:
Once a final variable has been assigned, it always contains the same value. If a final variable holds a reference to an object, then the state of the object may be changed by operations on the object, but the variable will always refer to the same object.
Three categories:
- Final fields (instance or static): assigned exactly once during construction or
<clinit>. - Final local variables: assigned exactly once before any read.
- Final parameters: cannot be reassigned within the method body.
The "exactly once" requirement is enforced by the definite assignment rules (JLS §16). Any read of a final variable that the compiler cannot prove is preceded by a write produces a compile error.
3. JLS §8.1.1.2 — Final classes¶
The class can have no subclasses. Records (§8.10) are implicitly final.
A final class: - Cannot be extended via extends. - Has the ACC_FINAL flag in its class file. - Plays well with the JIT (no CHA dependency).
Mutually exclusive with abstract (an abstract class must be extended) and with sealed (sealed allows specific subtypes, while final allows none).
record types are implicitly final per JLS §8.10:
A record class is implicitly final.
You cannot extend a record. You can implement interfaces.
4. JLS §8.4.3.3 — Final methods¶
A final method cannot be overridden or hidden.
A final method: - Cannot be overridden by a subclass. - Cannot be hidden by a subclass's static method (for static methods; though static methods can never be "overridden" anyway). - Has the ACC_FINAL flag in its class file.
Mutually exclusive with abstract (abstract requires override; final forbids it).
A private method is implicitly non-overridable (it's not visible to subclasses), so marking it final is redundant — though some style guides allow it.
5. JLS §8.3.1.2 — Final fields¶
A field can be declared final. Both class and instance variables (static and non-static fields) may be declared final.
Constraints:
- Must be definitely assigned (JLS §16) by the end of construction (instance fields) or static initialization (static fields).
- Cannot be reassigned after initial assignment.
- Cannot also be
volatile(mutually exclusive). - Cannot be a parameter or local that's reassigned.
A final instance field: - Default-initialized to its type's default value during object allocation. - Assigned by <init> (the constructor and instance initializer code). - Read freely by any method. - Verifier rejects putfield to a final instance field outside <init> of the same class.
A final static field: - Default-initialized during class preparation. - If a compile-time constant (primitive or String with constant initializer), the JVM uses the ConstantValue attribute to set it at link time, before <clinit>. - Otherwise assigned by <clinit>. - Verifier rejects putstatic to a final static field outside <clinit> of the same class.
6. JLS §17.5 — Final field semantics¶
The single most important section for concurrent Java:
Let
obe an object, andcbe a constructor foro's class in which a writewoccurs to afinalfield ofo. A "freeze" actionfonwoccurs at the end ofc. Note that if a constructor finishes by throwing an exception, the freeze still occurs. (...)If a thread reads a
finalfield after the freeze, it sees the value that was written.
Concretely:
- If thread T1 constructs an object
oand the constructor doesn't letoescape, then any thread T2 that observesoafter T1's constructor finishes is guaranteed to see allfinalfield values set during construction — without explicit synchronization. - Non-final fields don't get this guarantee. Without synchronization, T2 may see them at default values indefinitely.
The catch: escape. If T1 publishes o (e.g., assigns to a static field, registers with a callback, starts a thread that uses o) before the constructor finishes, T2 may see partially-constructed state. The freeze rule only protects post-construction observers.
7. JLS §15.28 / §4.12.4 — Compile-time constants¶
A constant variable is a static final variable of primitive or String type, initialized with a constant expression. The compiler:
- Inlines its value at every read site (no
getstatic). - Records the value in the class file's
ConstantValueattribute. - Allows constant folding in expressions involving it.
Example:
public class Limits {
public static final int MAX = 100;
public static final int DOUBLED = MAX * 2; // also a constant: 200
}
Both MAX and DOUBLED are constant variables. References to them in other classes are inlined.
Implications:
- Reading a constant does not trigger class initialization (JLS §12.4.1 explicitly excludes constant variable access from the trigger list).
- Updating a constant value requires recompiling all consumers.
- For values that may change between releases, prefer non-final or method-based exposure.
8. JVMS §4.1, §4.5, §4.6 — ACC_FINAL flag¶
The ACC_FINAL = 0x0010 flag bit appears in:
ClassFile.access_flagsfor final classes.field_info.access_flagsfor final fields.method_info.access_flagsfor final methods.
Constraints:
- Class:
ACC_FINALis mutually exclusive withACC_INTERFACEandACC_ABSTRACT. - Field:
ACC_FINALis mutually exclusive withACC_VOLATILE. - Method:
ACC_FINALis mutually exclusive withACC_ABSTRACT.
Inspect with javap -v MyClass.class. The flags appear as a hex value plus a comment listing the names.
9. JVMS §6.5 — Verifier rules for putfield/putstatic¶
For instance final fields (putfield, JVMS §6.5):
If the field is
final, it must be declared in the current class, and the instruction must occur in an instance initializer method (<init>) of the current class.
For static final fields (putstatic, JVMS §6.5):
If the field is
final, it must be declared in the current class, and the instruction must occur in the class initializer (<clinit>) of the current class.
The verifier rejects classes that violate these rules at class-load time, throwing VerifyError.
This is what makes "final fields are written exactly once" a hard JVM-level guarantee, not just a compiler suggestion.
10. Records' final fields (JVMS §4.7.30)¶
Records (since Java 16) introduce special treatment:
- The class is implicitly
final. - Component fields are
private final. - The canonical constructor's
<init>writes the components. - The
Recordattribute (JVMS §4.7.30) lists the components.
The verifier accepts the canonical constructor's writes because they happen in <init> of the same class.
Reflection's Class.getRecordComponents() returns metadata derived from the Record attribute. Frameworks like Jackson use this for JSON deserialization.
11. Sealed classes and final (JLS §8.1.1.4)¶
A sealed class declaration constrains its subclasses:
Each permitted subclass must declare exactly one of:
final— closes the line of inheritance.sealed(with its ownpermits) — restricts further extension.non-sealed— re-opens for extension.
The compiler enforces this at compile time. The verifier enforces it via the PermittedSubclasses attribute (JVMS §4.7.31) at class-load time — a class declaring a permits clause that doesn't match its actual subclasses is rejected.
12. JLS §16 — Definite assignment¶
The compiler proves at compile time that every read of a final variable is preceded by exactly one write. Example failure:
public class Foo {
private final int x;
public Foo(boolean cond) {
if (cond) x = 1;
// x might not be assigned if cond is false
}
}
Compile error: "variable x might not have been initialized."
Definite assignment rules:
- Direct assignments (
x = ...) count as definite. - Both branches of
if/elsemust assign. - Loop bodies that may not execute don't count.
- Method calls don't help — the compiler doesn't analyze them.
Use final to force yourself to think about every initialization path. The compiler is on your side.
13. The mutually exclusive table¶
| Modifier | Excludes |
|---|---|
final (class) | abstract, sealed, non-sealed |
final (method) | abstract |
final (field) | volatile |
sealed (class) | final, non-sealed |
non-sealed (class) | final, sealed |
abstract (class) | final |
abstract (method) | final, static, private, synchronized, native |
The compiler enforces these. Most are obvious; final + volatile is a common attempted misuse.
14. private is implicitly final-like¶
A private method: - Cannot be overridden (it's not visible to subclasses). - Is dispatched via invokespecial (direct, not virtual).
So private and final have similar runtime behavior for methods — both produce direct calls. Marking a private method final is technically allowed but stylistically redundant.
15. Reading order¶
- JLS §4.12.4 — final variables (foundational).
- JLS §8.3.1.2 — final fields.
- JLS §8.4.3.3 — final methods.
- JLS §8.1.1.2 — final classes.
- JLS §17.5 — final field memory model. (Critical for concurrent code.)
- JLS §16 — definite assignment.
- JVMS §4.5, §4.6 — class file flags.
- JVMS §6.5 — verifier rules.
These sections are short and dense. Reading them once is enough to handle 95% of final-related questions.
16. The takeaway¶
final has four distinct semantic faces:
- Compiler enforcement (definite assignment, no reassignment): JLS §4.12.4, §16.
- Verifier enforcement (no
putfield/putstaticon final fields outside<init>/<clinit>): JVMS §6.5. - JMM guarantee (freeze rule for safe publication): JLS §17.5.
- Compile-time constant inlining (for
static finalprimitives/strings): JLS §13.4.9, §15.28.
Each face is enforced independently — at compile time, link time, run time, and across compilation units. Together they make final one of Java's most rigorously enforced semantic commitments. Understand the four faces, and you understand final completely.