Senior 6 min · March 05, 2026

Java final Keyword—Mutable Reference Trap Data Loss

2% of production requests had duplicate payments because a final reference still allowed mutable access to a shared List.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • final locks a reference or prevents inheritance — not the object's contents
  • final variables: assigned once, compiler-enforced
  • final methods: subclasses cannot override behavior
  • final classes: no subclassing allowed (e.g. String, Integer)
  • Performance insight: final methods enable JIT inlining, saving ~5-10% in tight loops
  • Production insight: assuming final reference = immutable object leads to data corruption in multi-threaded pools
Plain-English First

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 ≠ Immutable
final List<String> 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.
Production Insight
A final static field in a shared library caused cache poisoning when two deployment versions loaded different class files with the same constant.
Rule: final static fields are compile-time constants only if they are primitives or String literals — not for complex objects.
Always rebuid dependent modules when changing final constants referenced in other compilation units.
Key Takeaway
final locks the reference, not the object.
Blank finals must be assigned in every constructor.
A final reference ≠ immutability — pair with immutable wrappers for true read-only.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 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 Pattern
The 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.
Production Insight
A security audit revealed that a library's authentication method was not final — a subclass overrode it to skip token validation for admin users.
Rule: any method that performs security checks, authorization, or audit logging should be declared final.
Cost of missing final: a single backdoor in production for months.
Key Takeaway
final methods protect critical algorithms from accidental or malicious override.
They enable JIT inlining for hot code paths.
Template Method pattern relies on a final method to enforce the algorithm skeleton.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 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.
Production Insight
A team made their value class non-final for flexibility. A subclass overrode equals() to ignore a field, causing hash-based sets to lose objects silently.
Rule: value types should be final. Non-final value types are a contract violation waiting to happen.
Cost: subtle data loss in production for weeks before discovery.
Key Takeaway
final classes prevent inheritance — useful for value types, utility classes, and security.
String is final to guarantee correctness in hash collections and thread safety.
Final classes hurt mockability — consider extracting interfaces for testability.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 Lambdas
Java 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.
Production Insight
A developer used a non-final parameter in a lambda captured in a listener — the lambda held a stale reference when the parameter was reassigned later in the method.
Rule: always declare parameters as final if they will be used in inner classes or lambdas.
Cost: bogus event handling that took two days to trace back to the reassignment.
Key Takeaway
final parameters prevent accidental reassignment and clarify intent.
Use them for lambdas and inner classes.
Effectively final variables work without the keyword, but explicit final documents the safety.

final and Immutability: The Two-Layer Protection Pattern

One of the biggest misconceptions about final is that it guarantees immutability. It does not. To get true immutability in Java, you need two layers: 1. final on the reference — ensures the variable always points to the same object. 2. An immutable object — ensures the object itself has no public mutator methods and all its fields are final and primitive or deeply immutable.

Layering these two gives you a guarantee that neither the reference nor the contents can change. This is why String is both final (class) and its backing char[] is private and final (field) — and the class exposes no setters.

When you need a mutable object but want to prevent the reference from being changed, use final on the variable. When you need an object that can never change, use final on the class, make all fields final, provide no setters, and defensively copy references in constructors. This combination is what the Java memory model calls 'thread-safe without synchronization' for immutable objects.

The practical upshot: do not confuse access control (won't change the pointer) with behavioural control (won't change the data). They solve different problems and you often need both.

MutableVsImmutable.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
import java.util.List;

public class MutableVsImmutable {

    public static void main(String[] args) {
        // Layer 1: final reference, mutable object
        final List<String> names = new ArrayList<>();
        names.add("Alice"); // allowed -- object changes
        // names = new ArrayList<>(); // compile error -- reference locked

        // Layer 2: final reference, immutable object
        final List<String> immutableNames = List.of("Alice", "Bob");
        // immutableNames.add("Charlie"); // throws UnsupportedOperationException

        // Deeply immutable custom class
        final io.thecodeforge.ImmutablePerson person =
            new io.thecodeforge.ImmutablePerson("Alice", 30);
        // person.getAge() always returns 30
        // Cannot change name or age
    }
}

// Truly immutable class
final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name; // String is already immutable
        this.age = age;   // primitive is always immutable
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}
Output
(No runtime output -- compile-only example)
Mental Model: The Two Locks
  • final reference: handle can't be moved to a different door.
  • Immutable object: nothing inside the room can be changed.
  • To lock down a room completely, lock both the handle and the contents.
  • Example: String locks the handle (final class) and the room (private final char[] with no setters).
  • A final List is a locked handle to a room with unlocked furniture.
Production Insight
A caching service used final Map<String, Object> but passed around mutable maps — cache invalidation became impossible because every consumer could mutate the stored data.
Rule: for shared caches, always use immutable maps (Map.copyOf) on top of final references.
Cost: stale cache poisoned responses for hours during peak traffic.
Key Takeaway
final == reference lock.
Immutable = object cannot change.
For guaranteed safety, use both: final reference to immutable object.
String is the canonical example of this two-layer protection.
● Production incidentPOST-MORTEMseverity: high

The Shared List That Lost Customer Data

Symptom
Production alerts showed duplicate payment processing and missing transaction logs for ~2% of requests. The list occasionally contained stale or duplicate entries.
Assumption
The team believed that declaring the reference as final would make the list thread-safe. They skipped synchronization because 'it's final, so nobody can change it.'
Root cause
final only locks the reference — the List object itself was mutable. Two threads concurrently called add() without synchronization, causing race conditions and internal corruption.
Fix
Replaced the mutable ArrayList with an immutable List created via List.copyOf() and documented that final does not imply immutability. Added proper synchronization for any truly shared collections.
Key lesson
  • final on a reference variable does NOT make the object immutable.
  • Always pair final with immutable wrappers (List.copyOf, Collections.unmodifiableList) or explicit synchronization when sharing across threads.
  • Document the guarantee: 'final reference, mutable contents' vs 'final reference, immutable contents'.
Production debug guideDiagnose compile-time and runtime problems caused by misuse of final4 entries
Symptom · 01
Compile error: 'variable X might not have been initialized'
Fix
Check every constructor path — final instance fields must be assigned in EVERY constructor. Use constructor chaining (this(…)) to centralize assignment.
Symptom · 02
Runtime exception: UnsupportedOperationException when calling add/remove on a final list
Fix
The list is likely an unmodifiable wrapper (e.g., List.of() or Collections.unmodifiableList). Check the declaration — final reference + immutable list means you cannot modify contents. If you need a mutable list, use new ArrayList<>() (still final reference, but contents changeable).
Symptom · 03
Unexpected behavior in a subclass attempting to override a final method
Fix
Compile error: 'cannot override final method'. Remove the final keyword from the parent method or redesign the class hierarchy to use composition instead of inheritance.
Symptom · 04
NullPointerException when accessing a final field in a constructor before it's assigned
Fix
final fields must be assigned exactly once and before any usage. If you use the field in a method called from the constructor, the field may still be null. Assign final fields early in the constructor, before calling any overridable methods.
★ Quick Fixes for Common final Keyword MistakesTroubleshoot blank finals, uninitialized fields, and false immutability assumptions.
Blank final instance field not assigned in a constructor branch
Immediate action
Locate every constructor — each must assign the field exactly once.
Commands
// Add assignment in the missing constructor path this.myField = value;
// Use constructor chaining to avoid duplication public MyClass(String val) { this(val, 0); } public MyClass(String val, int count) { this.myField = val; this.count = count; }
Fix now
Add the missing assignment in the constructor or chain to a main constructor that does the assignment.
Compile error: 'cannot assign a value to final variable' after reassignment+
Immediate action
Remove the reassignment. final variables can only be assigned once.
Commands
// Remove the second assignment final int max = 10; // max = 20; // REMOVE THIS
If you need to change the value, remove final and document the mutability.
Fix now
Delete the offending second assignment or change the variable from final to ordinary.
List modified unexpectedly despite being declared final+
Immediate action
Check if the list was created with List.of() or Collections.unmodifiableList() — these throw UnsupportedOperationException.
Commands
// Check creation site final List<String> items = new ArrayList<>(); // mutable final List<String> items = List.of("a"); // immutable
Add synchronization or use an immutable wrapper if needed.
Fix now
Replace List.of() with new ArrayList<>() if mutability is required, or add a comment clarifying that the reference is final but the contents are mutable.
Trying to mock a final class with Mockito and getting 'Cannot mock/spy class'+
Immediate action
Add the mock-maker-inline extension or refactor to depend on an interface.
Commands
// Add mock-maker-inline to test resources mockito-inline dependency
// Better: extract interface interface MyService { ... } class MyServiceFinal implements MyService { ... }
Fix now
If you cannot change the production code, add mockito-inline to the test classpath.
Comparison: final Variable, Method, and Class
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

1
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.
2
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.
3
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.
4
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.
5
final parameters clarify intent and are safe to capture in lambdas
consider using them in all public methods for documented immutability of the reference.

Common mistakes to avoid

4 patterns
×

Thinking final means the object is immutable

Symptom
Developer declares final List<String> 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.
×

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.
×

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.
Fix
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.
×

Using final on a method parameter but still modifying the object's state

Symptom
Method changes the object's fields but the parameter reference is declared final — gives false sense of safety.
Fix
Remember that final on a parameter only prevents reassignment of the reference. Document if the method mutates the object or use immutable parameters explicitly.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Can you explain the difference between a final variable holding a primit...
Q02SENIOR
Why is the String class declared final in Java? What security or correct...
Q03SENIOR
If a class is not final but all of its constructors are private, can it ...
Q01 of 03JUNIOR

Can 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?

ANSWER
For a primitive, final means the value cannot change after assignment. For an object reference, final means the reference cannot be reassigned to point to a different object, but the state of the object itself can still be modified (calling setter methods, adding to list, etc.). To make the object truly immutable, the class must be designed as immutable (all final fields, no setters, defensive copying).
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can a final variable be declared without being initialised immediately?
02
What is the difference between final and effectively final in Java?
03
Does making a method final improve performance in Java?
04
Can you mock a final class with Mockito?
🔥

That's OOP Concepts. Mark it forged?

6 min read · try the examples if you haven't

Previous
static Keyword in Java
13 / 16 · OOP Concepts
Next
Object Class in Java