Home Java The final Keyword in Java Explained — Variables, Methods and Classes

The final Keyword in Java Explained — Variables, Methods and Classes

In Plain English 🔥
Imagine you write your name on a birthday cake in icing — once it's set, nobody can scrape it off and write someone else's name. That's exactly what final does in Java: it locks something in place so nothing can change or override it later. A final variable is a value carved in stone, a final method is a recipe nobody is allowed to remix, and a final class is a blueprint you can use but can never extend.
⚡ Quick Answer
Imagine you write your name on a birthday cake in icing — once it's set, nobody can scrape it off and write someone else's name. That's exactly what final does in Java: it locks something in place so nothing can change or override it later. A final variable is a value carved in stone, a final method is a recipe nobody is allowed to remix, and a final class is a blueprint you can use but can never extend.

Most Java developers use final casually — slapping it on a constant here, accepting an IDE suggestion there — without really understanding what contract they're making. That's a missed opportunity, because final is one of the clearest ways to communicate intent in your code. When a teammate reads final, they instantly know: this was a deliberate decision, not an accident.

The problem final solves is subtle but critical: uncontrolled mutability and unexpected inheritance. Without final, any value can be quietly reassigned mid-method, any method can be silently overridden in a subclass, and any class can be extended in ways its original author never intended. These aren't hypothetical bugs — they're the source of real security vulnerabilities (the String class is final for exactly this reason) and the cause of countless hard-to-trace defects in large codebases.

By the end of this article you'll know exactly when and why to apply final to variables, method parameters, methods, and classes. You'll understand the difference between a final reference and a truly immutable object, dodge the two gotchas that catch almost every intermediate developer, and be ready to answer the tricky interview questions that separate candidates who memorise syntax from those who understand design.

final Variables — Locking a Value in Place (and What That Really Means)

A final variable can be assigned exactly once. After that first assignment, any attempt to reassign it is a compile-time error — the compiler catches it before your code ever runs. This is powerful because the protection is guaranteed, not just hoped for.

There are three flavours: final local variables (inside a method), final instance fields (per object), and final static fields (class-level constants). Each has a different point at which the 'one assignment' must happen.

For static final fields the assignment must happen either at the declaration site or inside a static initialiser block. For instance final fields it must happen either at the declaration site, inside an instance initialiser block, or in every constructor. The compiler tracks every possible code path and complains if any path leaves the field unassigned.

The trickiest part — and the one most developers misunderstand — is that final on a reference variable locks the reference, not the object it points to. A final List can still have items added to it. Final means 'this variable will always point to this object', not 'this object will never change'. Keep that distinction sharp.

FinalVariableDemo.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
import java.util.ArrayList;
import java.util.List;

public class FinalVariableDemo {

    // static final = a true compile-time constant shared across all instances
    // Convention: ALL_CAPS with underscores for static final constants
    private static final int MAX_LOGIN_ATTEMPTS = 3;

    // instance final field — must be set in every constructor
    private final String username;

    public FinalVariableDemo(String username) {
        // This is the ONE allowed assignment for this instance field
        this.username = username;
    }

    public void demonstrateFinalBehaviour() {

        // final local variable — assigned once, never changed
        final double taxRate = 0.2;

        double itemPrice = 49.99;
        double taxAmount = itemPrice * taxRate;

        // The line below would cause a compile error: cannot assign a value to final variable taxRate
        // taxRate = 0.25;  <-- COMPILE ERROR if you uncomment this

        System.out.println("Username     : " + username);
        System.out.println("Max attempts : " + MAX_LOGIN_ATTEMPTS);
        System.out.println("Tax rate     : " + taxRate);
        System.out.println("Tax on $" + itemPrice + " = $" + taxAmount);

        // KEY POINT: final reference != immutable object
        // The list reference is locked, but the list contents are NOT locked
        final List<String> sessionTokens = new ArrayList<>();
        sessionTokens.add("token-abc-123");   // perfectly legal — we're modifying the object
        sessionTokens.add("token-xyz-789");   // still legal

        // This WOULD be a compile error — reassigning the reference itself
        // sessionTokens = new ArrayList<>();  <-- COMPILE ERROR

        System.out.println("Session tokens: " + sessionTokens);
    }

    public static void main(String[] args) {
        FinalVariableDemo demo = new FinalVariableDemo("alice");
        demo.demonstrateFinalBehaviour();
    }
}
▶ Output
Username : alice
Max attempts : 3
Tax rate : 0.2
Tax on $49.99 = $9.998
Session tokens: [token-abc-123, token-xyz-789]
⚠️
Watch Out: final ≠ Immutablefinal List items locks the reference — items will always point to the same List object. But items.add("anything") still works fine. If you want a truly read-only list, use Collections.unmodifiableList() or List.of() on top of final.

final Methods — Sealing a Behaviour Your Subclasses Can't Override

When you mark a method final, you're telling every subclass: 'You can inherit this behaviour, but you cannot replace it.' That's a powerful design statement.

The most common reason to do this is correctness: some methods encode logic so fundamental to how the class works that allowing a subclass to override them would break guarantees the class depends on. The classic example is the equals/hashCode contract — if a framework class defines a final equals() it's protecting the integrity of hash-based collections.

The second reason is security. If a class handles authentication or encryption, a rogue subclass overriding a critical method could silently bypass security checks. Making those methods final closes that door entirely.

Final methods also give the JIT compiler a hint. Because the compiler knows at runtime there's exactly one version of a final method, it can inline the call — replacing the method invocation with the method body directly — which can improve performance in tight loops. This is a micro-optimisation in most apps, but it's the real reason some core Java library methods are final.

Note that a final method in a non-final class is completely normal. You're locking one specific behaviour while still allowing the class to be extended in other ways.

PaymentProcessor.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// Base class — defines a payment workflow
class PaymentProcessor {

    private static final double FRAUD_THRESHOLD = 10_000.00;

    // This method defines steps that MUST happen in this order for every payment.
    // Subclasses can customise HOW they charge, but not WHAT checks surround it.
    public final boolean processPayment(String customerId, double amount) {

        // Step 1: fraud check — no subclass can skip or alter this
        if (!passesFraudCheck(customerId, amount)) {
            System.out.println("[BLOCKED] Fraud check failed for customer: " + customerId);
            return false;
        }

        // Step 2: delegate to subclass-specific charge logic
        boolean charged = chargeCustomer(customerId, amount);

        // Step 3: audit log — no subclass can skip this either
        auditLog(customerId, amount, charged);

        return charged;
    }

    // private fraud check — already hidden from subclasses
    private boolean passesFraudCheck(String customerId, double amount) {
        // Simplified: flag anything over the threshold
        return amount <= FRAUD_THRESHOLD;
    }

    // Subclasses override THIS to provide their own charging mechanism
    protected boolean chargeCustomer(String customerId, double amount) {
        System.out.println("[DEFAULT] Charging $" + amount + " to customer: " + customerId);
        return true;
    }

    private void auditLog(String customerId, double amount, boolean success) {
        System.out.println("[AUDIT] Customer: " + customerId
                + " | Amount: $" + amount
                + " | Success: " + success);
    }
}

// Subclass provides a Stripe-specific charge implementation
class StripePaymentProcessor extends PaymentProcessor {

    @Override
    protected boolean chargeCustomer(String customerId, double amount) {
        System.out.println("[STRIPE] Sending charge request for $" + amount
                + " — customer: " + customerId);
        return true; // simulate a successful Stripe API call
    }

    // The line below would cause a compile error:
    // Cannot override the final method from PaymentProcessor
    //
    // @Override
    // public boolean processPayment(String customerId, double amount) { ... }
}

public class PaymentProcessor {
    public static void main(String[] args) {
        PaymentProcessor stripe = new StripePaymentProcessor();

        System.out.println("--- Transaction 1: Normal amount ---");
        stripe.processPayment("cust-001", 149.99);

        System.out.println();
        System.out.println("--- Transaction 2: Suspicious amount ---");
        stripe.processPayment("cust-002", 15_000.00);
    }
}
▶ Output
--- Transaction 1: Normal amount ---
[STRIPE] Sending charge request for $149.99 — customer: cust-001
[AUDIT] Customer: cust-001 | Amount: $149.99 | Success: true

--- Transaction 2: Suspicious amount ---
[BLOCKED] Fraud check failed for customer: cust-002
⚠️
Pro Tip: Template Method PatternThe code above is a textbook Template Method pattern — a final method defines the algorithm skeleton while protected non-final methods let subclasses fill in specific steps. This is one of the most practical uses of final methods in real-world OOP design.

final Classes — Why String, Integer and Math Are All Sealed

A final class cannot be subclassed. Full stop. Any attempt to extend it produces an immediate compile error. This is the most drastic use of final, and it should be a deliberate, considered decision.

The Java standard library uses this extensively. String is final because if it weren't, a malicious or careless developer could create a subclass that overrides equals() or hashCode() in inconsistent ways, quietly breaking every HashMap or Set that holds strings. Integer, Double, and all other wrapper types are final for the same reason. Math is final because it's a pure utility class — there's nothing meaningful to extend.

When should you make your own classes final? Consider it when: the class is a pure value type (like a Money or Coordinate class), when it's a utility class with only static methods, or when correctness of the entire system depends on the class behaving in exactly one way. Immutability and final often go together — if you're building a truly immutable class (all fields are final, no setters, defensive copies in the constructor), marking the class final is the last line of defence against a subclass introducing mutable state.

The flip side: final classes hurt testability. You can't mock a final class with most mocking frameworks without extra configuration. So final should be intentional, not reflexive.

Money.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// final class — this is an immutable value type.
// Making it final prevents subclasses from adding mutable fields
// or overriding equals/hashCode in ways that break collections.
public final class Money {

    private final String currencyCode;  // e.g. "USD", "EUR"
    private final long   amountInCents; // store as cents to avoid floating-point errors

    public Money(String currencyCode, long amountInCents) {
        if (currencyCode == null || currencyCode.isBlank()) {
            throw new IllegalArgumentException("Currency code must not be blank");
        }
        if (amountInCents < 0) {
            throw new IllegalArgumentException("Amount must not be negative");
        }
        // Both fields are final — assigned here and never changed
        this.currencyCode  = currencyCode.toUpperCase();
        this.amountInCents = amountInCents;
    }

    // Returns a NEW Money object — the original is never mutated
    public Money add(Money other) {
        if (!this.currencyCode.equals(other.currencyCode)) {
            throw new IllegalArgumentException(
                "Cannot add " + this.currencyCode + " and " + other.currencyCode);
        }
        return new Money(this.currencyCode, this.amountInCents + other.amountInCents);
    }

    public String getFormattedAmount() {
        // Divide by 100 to display as decimal dollars/euros/etc.
        return currencyCode + " " + String.format("%.2f", amountInCents / 100.0);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Money)) return false;
        Money other = (Money) obj;
        // Because the class is final, we KNOW obj is exactly Money — not a sneaky subclass
        return this.amountInCents == other.amountInCents
            && this.currencyCode.equals(other.currencyCode);
    }

    @Override
    public int hashCode() {
        return 31 * currencyCode.hashCode() + Long.hashCode(amountInCents);
    }

    @Override
    public String toString() {
        return getFormattedAmount();
    }

    // --- Attempting to extend Money would cause a compile error: ---
    // class TaxedMoney extends Money { }  <-- COMPILE ERROR: cannot inherit from final Money

    public static void main(String[] args) {
        Money productPrice  = new Money("USD", 1999); // $19.99
        Money shippingCost  = new Money("USD",  599); // $5.99
        Money totalCost     = productPrice.add(shippingCost);

        System.out.println("Product price  : " + productPrice);
        System.out.println("Shipping cost  : " + shippingCost);
        System.out.println("Total          : " + totalCost);

        // Prove immutability — productPrice is unchanged after add()
        System.out.println("Original price still: " + productPrice);

        Money anotherTotal = new Money("USD", 2598);
        System.out.println("Equals check   : " + totalCost.equals(anotherTotal));
    }
}
▶ Output
Product price : USD 19.99
Shipping cost : USD 5.99
Total : USD 25.98
Original price still: USD 19.99
Equals check : true
🔥
Interview Gold: Why Is String Final in Java?String is final to ensure security (class loaders rely on immutable strings), thread safety (immutable objects are inherently thread-safe), and performance (String interning and caching hashCode only work if the value can never change). This is a favourite interview question — now you have the full answer.

final Method Parameters — A Habit Worth Building

You can mark method parameters as final too. This means the parameter variable itself can't be reassigned inside the method body. The object it refers to can still be modified — same rule as final local variables.

This isn't enforced by the JVM at runtime — it's a compile-time guard for you and your teammates. Its biggest value is clarity and bug prevention in longer methods. When you see a final parameter, you know immediately: this variable represents the input that came in, not some transformed version of it. There's no guessing whether the method changed the reference partway through.

It's especially useful in anonymous inner classes and lambda-adjacent code. Before Java 8, any local variable captured by an anonymous inner class had to be explicitly final. From Java 8 onwards, the rule relaxed to 'effectively final' (never reassigned, even without the keyword), but the intent is the same.

Some teams enforce final on all method parameters via a code style rule (Checkstyle supports this). Others consider it visual noise. The pragmatic middle ground: use it when a method is long enough that parameter shadowing is a real risk, and always use it when you're capturing the parameter in an inner class or lambda.

OrderCalculator.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
import java.util.List;

public class OrderCalculator {

    // final parameters — the references 'items' and 'discountPercent' cannot be
    // reassigned inside this method. Prevents accidental shadowing in long methods.
    public double calculateTotal(final List<String> items, final double discountPercent) {

        // This would be a compile error:
        // discountPercent = 0.0;  <-- COMPILE ERROR: cannot assign a value to final variable

        double subtotal = items.size() * 9.99; // simplified: each item is $9.99
        double discount = subtotal * (discountPercent / 100.0);
        double total    = subtotal - discount;

        System.out.println("Items ordered  : " + items.size());
        System.out.println("Subtotal       : $" + String.format("%.2f", subtotal));
        System.out.println("Discount (" + discountPercent + "%) : -$" + String.format("%.2f", discount));
        System.out.println("Total          : $" + String.format("%.2f", total));

        return total;
    }

    // Demonstrating 'effectively final' in a lambda capture
    public void printItemsAsync(final List<String> items) {
        // 'items' is final — safe to capture in the lambda below
        Runnable printer = () -> {
            for (String item : items) {
                System.out.println("  - " + item);
            }
        };
        printer.run();
    }

    public static void main(String[] args) {
        OrderCalculator calculator = new OrderCalculator();

        List<String> cart = List.of("Java Book", "Mechanical Keyboard", "USB Hub");

        System.out.println("=== Order Summary ===");
        calculator.calculateTotal(cart, 10.0); // 10% discount

        System.out.println();
        System.out.println("=== Your Items ===");
        calculator.printItemsAsync(cart);
    }
}
▶ Output
=== Order Summary ===
Items ordered : 3
Subtotal : $29.97
Discount (10.0%) : -$3.00
Total : $26.97

=== Your Items ===
- Java Book
- Mechanical Keyboard
- USB Hub
⚠️
Pro Tip: Effectively Final in LambdasJava 8+ lets you capture a variable in a lambda without writing final, as long as it's never reassigned after initialisation. The compiler calls this 'effectively final'. But if you need to reassign the variable later in the same method, the capture breaks and you'll get: 'Variable used in lambda expression should be final or effectively final'. Fix: extract the lambda to a separate method, or use an AtomicReference wrapper.
Aspectfinal Variablefinal Methodfinal Class
What it preventsReassigning the variable referenceSubclasses overriding the methodAny class extending this class
Compile error triggerSecond assignment to the variable@Override in a subclassextends FinalClassName
Runtime costNone — compile-time onlyEnables JIT inlining (minor speedup)None — compile-time only
Applies to objects?Locks reference, NOT object contentsN/AN/A
Common real-world useConstants, immutable fieldsTemplate Method pattern, securityValue types, utility classes
Java stdlib examplesMath.PI, Integer.MAX_VALUEObject.getClass()String, Integer, Math

🎯 Key Takeaways

  • final on a variable locks the reference, not the object — a final List can still have items added; use Collections.unmodifiableList() or List.of() if you need the contents locked too.
  • final methods protect critical algorithm steps from being overridden by subclasses — this is the engine behind the Template Method design pattern and is essential for security-sensitive code.
  • final classes are a deliberate design choice for value types and utility classes — String, Integer, and Math are final to guarantee correctness in collections and across threads, not just for performance.
  • A final instance field must be assigned in every constructor — miss one constructor path and the compiler will tell you immediately, which is one of the few times Java's strictness saves you from a nasty NPE.

⚠ Common Mistakes to Avoid

  • Mistake 1: Thinking final means the object is immutable — Symptom: developer declares final List names and then wonders why names.add('Alice') compiles fine. The final keyword locks the reference (names will always point to the same List), not the object's contents. Fix: for a truly read-only list, use List.of() or Collections.unmodifiableList() and assign that to a final variable. Both layers are needed.
  • Mistake 2: Forgetting that final instance fields must be assigned in EVERY constructor — Symptom: compile error 'variable userId might not have been initialised' when you add a second constructor and forget to assign the final field in it. Fix: always assign every final field in every constructor, or use constructor chaining (this(...)) to funnel all construction through one path that does the assignment.
  • Mistake 3: Making a class final and then discovering you can't mock it in tests — Symptom: Mockito throws 'Cannot mock/spy class X because it is final' and the test suite breaks. This is especially painful when you add final to a class late in the project. Fix: either use Mockito's MockMaker inline extension (mockito-inline dependency) which CAN mock final classes, or — better design — depend on an interface rather than the concrete final class, so tests can swap in a test double.

Interview Questions on This Topic

  • QCan you explain the difference between a final variable holding a primitive and a final variable holding an object reference? What can and can't you change in each case?
  • QWhy is the String class declared final in Java? What security or correctness problems would arise if it weren't?
  • QIf a class is not final but all of its constructors are private, can it be subclassed? How does this compare to making the class explicitly final — and when would you choose one approach over the other?

Frequently Asked Questions

Can a final variable be declared without being initialised immediately?

Yes — this is called a blank final variable. For instance fields, the assignment must happen in every constructor. For static fields, it must happen in a static initialiser block. For local variables, you must assign before the first use. The compiler tracks every code path and will refuse to compile if any path leaves a final variable unassigned.

What is the difference between final and effectively final in Java?

A final variable is explicitly declared with the final keyword and the compiler enforces that it's only assigned once. An effectively final variable has no final keyword but is never reassigned after its initial assignment — the compiler recognises it as safe to capture in lambdas and anonymous inner classes. The runtime behaviour is identical; the difference is only whether you write the keyword or not.

Does making a method final improve performance in Java?

Potentially, yes, but in modern Java the JIT compiler is smart enough to devirtualise and inline method calls even without the final keyword when it can prove at runtime that there's only one implementation. So final for performance is rarely necessary today. The real reasons to use final on methods are design clarity and security — preventing subclasses from changing behaviour you need to rely on.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← Previousstatic Keyword in JavaNext →Strings in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged