final Keyword

Su Suriya Ravichandran Updated 14 Nov 2026
8 min read ·Lesson 49 of 133

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 toEffect
Local variableCan be assigned only once
ParameterCannot be reassigned inside the method
FieldMust be assigned exactly once, at declaration or in every constructor
static final fieldA class-level constant
MethodCannot be overridden by subclasses
ClassCannot 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 Money could be subclassed, a subclass could add mutable fields or override methods to behave inconsistently, breaking every assumption code makes about Money objects.
  • Security. String is final, so no code can pass a malicious String subclass 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about final Keyword and get answers drawn from it.

Signed-in readers only.