final Keyword
The final keyword means "cannot be changed after it is set." Its precise effect depends on what it is applied to: a final variable cannot be reassigned, a final method cannot be overridden, and a final class cannot be extended. In every case, final turns a design intention into a rule the compiler enforces.
| Applied to | Effect |
|---|---|
| Local variable | Can be assigned only once |
| Parameter | Cannot be reassigned inside the method |
| Field | Must be assigned exactly once, at declaration or in every constructor |
static final field | A class-level constant |
| Method | Cannot be overridden by subclasses |
| Class | Cannot be extended |
final Local Variables
A final local variable can be assigned once and never again:
final int maxAttempts = 3;
maxAttempts = 5; // compile error: cannot assign a value to final variable maxAttempts
The assignment does not have to happen at the declaration, as long as it happens exactly once on every path before the variable is used:
final String grade;
if (score >= 90) {
grade = "A";
} else if (score >= 75) {
grade = "B";
} else {
grade = "C";
}
System.out.println(grade);
The compiler verifies that grade is assigned on every branch and never twice. If one branch forgot to assign it, the println would not compile. This turns final into a correctness check: a reader knows the value cannot change after this block.
Effectively final
A local variable that is never reassigned after initialization is effectively final, even without the keyword. Lambdas and anonymous classes may only use local variables from the enclosing scope that are final or effectively final:
int threshold = 50;
List<Integer> high = scores.stream()
.filter(s -> s > threshold) // allowed: threshold is effectively final
.toList();
threshold = 60; // now threshold is NOT effectively final,
// so the lambda above no longer compiles
The lambda may run later, possibly on another thread, so Java captures the variable's value. Allowing the original to change would make the captured copy and the variable disagree, so the language forbids it.
final Parameters
A final parameter cannot be reassigned inside the method:
public double applyDiscount(final double price, final double percent) {
price = price * 0.9; // compile error
return price * (100 - percent) / 100;
}
Some teams mark all parameters final to guarantee they always hold the caller's values; others rely on the convention of never reassigning parameters. Either way, reassigning a parameter is rarely a good idea, because readers expect it to hold what was passed in.
final Fields
A final field must be assigned exactly once: either where it is declared, in an initializer block, or in every constructor. After construction, it can never change.
public class Employee {
private final String employeeId; // assigned in the constructor
private final LocalDate joined = LocalDate.now(); // assigned at declaration
private String department; // can change: transfers happen
public Employee(String employeeId, String department) {
this.employeeId = employeeId;
this.department = department;
}
public void transfer(String newDepartment) {
this.department = newDepartment;
}
public void changeId(String id) {
this.employeeId = id; // compile error: final field
}
}
The choice of what to make final documents the domain: an employee's ID and joining date never change, but their department can. Anyone reading the class learns this from the declarations alone.
The compiler checks every constructor:
public Employee(String employeeId) {
// forgot to assign... compile error: variable employeeId might not have been initialized
}
This also catches the common bug where a constructor assigns a parameter to itself (employeeId = employeeId; instead of this.employeeId = employeeId;), because the final field then remains unassigned.
final References Versus Immutable Objects
final on a reference variable prevents the variable from pointing to a different object. It does not prevent the object itself from changing:
final List<String> tags = new ArrayList<>();
tags.add("java"); // allowed: modifies the list object
tags.add("oop");
tags = new ArrayList<>(); // compile error: reassigns the variable
final int[] scores = {80, 90};
scores[0] = 100; // allowed: array contents can change
tags (final) ───────► [ "java", "oop" ] ← contents can change
└── the arrow itself cannot be redirected
To make the contents unchangeable too, use an immutable object:
final List<String> tags = List.of("java", "oop");
tags.add("streams"); // UnsupportedOperationException at runtime
The distinction matters when building immutable classes: a private final List<String> field still needs defensive copying, or an unmodifiable list, to keep callers from altering the object's state.
static final Constants
Combining static and final creates a class-level constant: one shared copy that never changes.
public class Limits {
public static final int MAX_UPLOAD_MB = 25;
public static final double LATE_FEE_PER_DAY = 5.0;
public static final String SUPPORT_EMAIL = "support@example.com";
}
Constants are named in UPPER_SNAKE_CASE. Replacing unexplained literals ("magic numbers") with named constants makes code self-documenting and ensures that changing a value means editing one line:
if (fileSizeMb > 25) { ... } // what is 25?
if (fileSizeMb > Limits.MAX_UPLOAD_MB) { ... } // clear
Compile-time constants are inlined
A static final field of primitive or String type initialized with a constant expression is a compile-time constant. The compiler copies its value directly into every class that uses it:
// Config.java
public static final int TIMEOUT = 30;
// Client.java (compiled against Config)
connect(Config.TIMEOUT); // compiled as connect(30)
If Config is later changed to TIMEOUT = 60 and only Config.java is recompiled, Client still uses 30. Recompile all dependent classes after changing a constant; build tools do this automatically. Values that may change between releases are better read from configuration than hard-coded as constants.
final Methods
A final method cannot be overridden by a subclass:
public class Account {
private double balance;
public final void withdraw(double amount) {
if (amount > balance) {
throw new IllegalStateException("Insufficient funds");
}
balance -= amount;
audit("WITHDRAW", amount);
}
protected void audit(String action, double amount) {
System.out.println(action + " " + amount);
}
}
public class SavingsAccount extends Account {
@Override
public void withdraw(double amount) { ... } // compile error: withdraw is final
@Override
protected void audit(String action, double amount) { // allowed
System.out.println("[SAVINGS] " + action + " " + amount);
}
}
withdraw contains a rule (no overdrafts) that subclasses must not bypass, so it is final. audit is intentionally left open so subclasses can customize logging. Making a method final is a statement that its behavior is part of the class's guarantees.
Methods called from a constructor should be final or private; otherwise a subclass override could run before the subclass's fields are initialized.
private methods and static methods cannot be overridden anyway, so adding final to them has no additional effect.
final Classes
A final class cannot be extended:
public final class Money {
private final long paise;
public Money(long paise) { this.paise = paise; }
public Money plus(Money other) { return new Money(paise + other.paise); }
public long getPaise() { return paise; }
}
public class FakeMoney extends Money { } // compile error: cannot inherit from final Money
Reasons to make a class final:
- Immutability guarantees. If
Moneycould be subclassed, a subclass could add mutable fields or override methods to behave inconsistently, breaking every assumption code makes aboutMoneyobjects. - Security.
Stringisfinal, so no code can pass a maliciousStringsubclass that changes its value after a security check. - Design clarity. A class not designed with extension in mind is safer closed. Designing for inheritance requires documenting which methods subclasses may override and how they interact.
String, Integer and the other wrapper classes, LocalDate, and Math are all final. Records and enums are implicitly final as well.
Java 17 added sealed classes, a middle ground that allows extension only by a specific list of subclasses. They are covered alongside inheritance.
Building an Immutable Class
final is a key ingredient of immutable classes, whose objects cannot change after construction:
import java.util.List;
public final class Invoice { // 1. class cannot be subclassed
private final String number; // 2. all fields private and final
private final List<String> items;
public Invoice(String number, List<String> items) {
this.number = number;
this.items = List.copyOf(items); // 3. defensive, unmodifiable copy
}
public String getNumber() { return number; }
public List<String> getItems() { return items; } // already unmodifiable
public Invoice withItem(String item) { // 4. "modifications" return new objects
List<String> updated = new java.util.ArrayList<>(items);
updated.add(item);
return new Invoice(number, updated);
}
}
List<String> source = new java.util.ArrayList<>(List.of("Laptop"));
Invoice inv = new Invoice("INV-001", source);
source.add("Mouse"); // does not affect inv
Invoice inv2 = inv.withItem("Mouse");
System.out.println(inv.getItems()); // [Laptop]
System.out.println(inv2.getItems()); // [Laptop, Mouse]
Each final plays a role: the class cannot be subclassed into something mutable, the fields cannot be reassigned, and the copied list cannot be modified by the caller who passed it in. Immutable objects are safe to share between threads, safe to use as HashMap keys, and easy to reason about. Records provide much of this automatically, though their components still need defensive copying when they are mutable collections.
Practical Example: Pricing Rules
public class PricingEngine {
public static final double GST_RATE = 0.18;
public static final double FREE_SHIPPING_THRESHOLD = 999;
public static final double SHIPPING_FEE = 49;
public final double total(final double subtotal) {
final double shipping = subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
final double discount = discountFor(subtotal);
final double taxable = subtotal - discount;
return taxable + taxable * GST_RATE + shipping;
}
protected double discountFor(double subtotal) {
return 0; // subclasses may offer discounts
}
}
class FestivalPricing extends PricingEngine {
@Override
protected double discountFor(double subtotal) {
return subtotal >= 2000 ? subtotal * 0.10 : 0;
}
}
PricingEngine normal = new PricingEngine();
PricingEngine festival = new FestivalPricing();
System.out.printf("%.2f%n", normal.total(2500)); // 2950.00
System.out.printf("%.2f%n", festival.total(2500)); // 2655.00
System.out.printf("%.2f%n", festival.total(500)); // 639.00
The rates are static final constants: shared, named, and unchangeable. total is a final method, so every pricing variant applies GST and shipping in the same order; subclasses cannot skip tax. discountFor is deliberately left non-final as the single extension point. Inside total, each intermediate value is a final local, which makes the calculation easy to follow: each name is assigned once and means one thing.