The final keyword

Some values should never change once set: a GST rate, an account number, the rules of a payment flow. The final keyword lets the compiler guarantee that. You'll see final variables and fields (including blank finals), effectively final variables, final parameters, final methods and classes, the difference between a final reference and an immutable object, and then practise with 10 problems.

Module 7 · OOP Pillars⏱ 21 minIntermediate10 practice problems
1 The story

The GST rate that changed overnight

Riya's billing app has static double GST_RATE = 0.18;. A teammate, testing a luxury-goods feature, writes GST_RATE = 0.28; inside one method. From then on, every bill the app prints charges 28% GST, including for tea and notebooks.

Without final static double GST_RATE 0.18 → 0.28 (!) GST_RATE = 0.28; // compiles every later bill is wrong With final static final double GST_RATE 0.18 forever error: cannot assign a value to final variable GST_RATE the mistake never leaves the laptop myphatshala.in
Riya

Sir, it was meant to be a constant. How did one line in a test method change it for everyone?

Arjun Sir

Because nothing told Java it was a constant. Add final: static final double GST_RATE = 0.18;. Then that line won't even compile.

Riya

Is final only for constants?

Arjun Sir

No. On a variable it means "assign once"; on a method, "no overriding"; on a class, "no subclasses". One word, three kinds of lock, all checked by the compiler.

2 Definition

What does final mean?

Formal definition (write this in exams)

final is a non-access modifier. A final variable (local, parameter, instance or static field) can be assigned exactly once; a final field with no initializer is a blank final and must be definitely assigned in every constructor (or a static initializer, for static fields). A final method cannot be overridden by subclasses. A final class cannot be extended. For a reference variable, final fixes the reference, not the state of the object it refers to.

In simple words

final variable: write once. final method: children can't change it. final class: no children at all. A final reference is a label glued to one box; you can still change what's inside the box.

Words you'll meet in this lesson

Constantstatic final field, named in CAPITALS: GST_RATE.
Blank finalA final field declared without a value; each constructor must set it once.
Effectively finalA local variable never reassigned, even without the keyword (Java 8).
final parameterA parameter the method body can't reassign.
final methodA method subclasses can't override.
final classA class nobody can extend, like String.

final at a glance

Local variableAssign once: final int x = 5; x = 6; → cannot assign a value to final variable x
Instance fieldSet at declaration, in an initializer block, or in every constructor. Missing → variable rate might not have been initialized
Static fieldSet at declaration or in a static block. Together, static final = a class constant.
Parameterfinal double amount: final parameter amount may not be assigned
MethodCan't be overridden: overridden method is final. Still inherited and callable.
ClassCan't be extended: cannot inherit from final String. All its methods are effectively final.

Exam tip: final, finally and finalize are a favourite trick question. final is this keyword; finally is a block that always runs after try/catch (Lesson 9.2); finalize() was an Object method for cleanup before garbage collection, deprecated since Java 9.

3 Types and ways to use it

Three places to put final

final variable

Assign once. Constants, IDs, values fixed for an object's life.

write once

final method

Children inherit it but can't override it: mandatory steps stay mandatory.

no overriding

final class

No subclasses at all. String, Integer, Math are final.

no extends

All three, plus a blank final

final class Invoice {                          // final class: can't be extended
    static final double GST_RATE = 0.18;       // constant
    private final String number;               // blank final: set in the constructor
    private final double amount;

    Invoice(String number, double amount) {
        this.number = number;                  // assigned exactly once
        this.amount = amount;
    }

    final double total() {                     // final method (already implied by final class)
        return amount + amount * GST_RATE;
    }

    String line() {
        return number + ": Rs " + total();
    }
}

public class Main {
    public static void main(String[] args) {
        final Invoice inv = new Invoice("INV-1042", 2000);
        System.out.println(inv.line());
        final int[] qty = {2, 3};
        qty[0] = 5;                            // allowed: the array object can change
        System.out.println(qty[0] + qty[1]);
    }
}
INV-1042: Rs 2360.0 8
final int[] a = {1, 2};Immutable object (e.g. String, Lesson 7.1)
a = new int[3];Compile error: the reference is fixed(The same, if the variable is final)
a[0] = 99;Allowed: the array's contents can changeImpossible: no method changes the state
What's protectedWhich object the variable points toThe object's data itself
How to get bothA final reference to an immutable object (or an unmodifiable copy)

The final-reference trap: private final List<String> items stops items = …, but items.add(…) and items.clear() still work. final is not immutability. To protect the contents, store and return unmodifiable copies (Lesson 7.1).

4 Step by step

How javac enforces final

  1. A final local is assigned once

    final double rate;
    if (isSenior) rate = 7.5; else rate = 7.0;   // OK: exactly one assignment on each path
    javac does definite assignment analysis: on every path through the code, a final variable must be assigned once before use and never twice ("might already have been assigned").
  2. A blank final field is set in every constructor

    private final String accountNo;
    Account(String no) { accountNo = no; }
    Account() { this("SB-NEW"); }                 // delegates, so still assigned once
    If any constructor can finish without assigning it, javac reports "variable accountNo might not have been initialized". With no constructor at all: "not initialized in the default constructor".
  3. Effectively final for lambdas and inner classes

    int limit = 5000;                        // never reassigned → effectively final
    Runnable r = () -> System.out.println(limit);
    A lambda captures a copy of the local's value. Java requires the local to be final or effectively final so the copy can never disagree with the original. Add limit++ anywhere and javac reports "must be final or effectively final".
  4. final methods lock the steps

    final String process() {
        return validate() + " → " + charge() + " → receipt";
    }
    Subclasses still inherit and call process(), but can't replace it, so a step like validation can't be skipped. This is how template methods are protected (Lesson 7.6).
  5. final classes close the hierarchy

    class SafeString extends String { }      // ✗ cannot inherit from final String
    String is final so that nobody can create a subclass that breaks its immutability, which the string pool, hashing and security checks all rely on.
5 See it happen

What final locks, and what it doesn't: both views

Left: the code and what javac or the console says. Right: the variable, its fixed reference, and the object it points to.

final in six lines 6 steps
What you see final double GST = 0.18; GST = 0.28; Account a = new Account("SB-1001"); Account() { } // forgot accountNo final int[] marks = {70, 80}; marks[0] = 95; marks = new int[2]; class SafeString extends String { } a.getAccountNo() → SB-1001 [95, 80] error: cannot assign a value to final variable GST error: variable accountNo might not have been initialized error: cannot assign a value to final variable marks error: cannot inherit from final String A second assignment is rejected. A blank final, set once in the constructor. Every constructor must set it. final reference, but the array changed! Pointing it elsewhere is what's blocked. final classes can't have children. myphatshala.in
Behind the screen Stack Heap GST 0.18 0.28 ✗ never stored a → #1 #1 Account accountNo "SB-1001" accountNo ??? (unset) marks → #2 #2 int[2] 95 80 new int[2] final class String SafeString ✗ javac rejects it; often inlined as a constant. Set once by the constructor, then read-only. Definite-assignment check fails at compile time. The lock is on the arrow, not on the boxes. A new arrow would break the lock: refused. No subclass can weaken String's guarantees. myphatshala.in
6 Real-world use

final in real systems

Experienced Java developers use final widely, because code that can't change by accident is easier to trust.

Watch it with an example: a payroll run that can't be tampered with

A payroll module fixes the PF rate, the holiday list reference and the run steps with final. Watch what final blocks, and what it doesn't.

final in a payroll run 6 steps
Animated example static final double PF = 0.12; PF = 0.10; holidays.add("Diwali"); holidays = new ArrayList<>(); List.copyOf(holidays).add("Holi"); class FastPayroll extends Payroll PF = 0.12 holidays → #1 #1 ArrayList [] [Diwali] copy: unmodifiable final run() { … } net on Rs 30,000 = Rs 26,400.0 error: cannot assign a value to final variable PF allowed: the list object changes error: cannot assign a value to final variable holidays java.lang.UnsupportedOperation Exception: the copy refuses error: run() in FastPayroll cannot override run() in Payroll; overridden method is final A constant fixes the PF rate for the whole run. A second assignment never compiles. final fixes the arrow, not the list's contents. Pointing it at another list is what's blocked. To protect contents, hand out unmodifiable copies. No subclass can skip the payroll steps. myphatshala.in
static final intMAX_OTP_TRIES = 3;static final …

1. Business constants

Limits and fixed rates (maximum OTP attempts, a free-delivery threshold) are static final, so no code path can quietly change them. Values that change by environment go in configuration instead.

OTP check→tries < MAX_OTP_TRIES→same rule everywhere
private finalRepository repo;

2. Dependencies set in the constructor

Service classes keep their collaborators in private final fields assigned in the constructor. The service can never end up half-wired or switched to another object mid-request.

new OrderService(repo, sms)→final fields→fixed for life
final classString, Integer,LocalDate

3. Immutable value classes

The JDK's String, wrapper classes and java.time types are final classes with final fields. That's what makes them safe as map keys and safe to share between threads.

map.put(key, …)→String can't change→key stays findable
final process()

4. Protected workflows

Base classes in payment and security code mark workflow methods final, so subclasses can customise steps but can't skip audits or validations.

Gateway subclass→can't override process()→audit always runs

Examples are simplified. Some teams mark every local variable and parameter final as a style rule; others rely on "effectively final" and keep final for fields, methods and classes.

7 Run the code

A bank account that can't be tampered with

class Account {
    static final double MIN_BALANCE = 1000;     // constant for all accounts
    private final String accountNo;             // blank final
    private double balance;

    Account(String accountNo, double opening) {
        this.accountNo = accountNo;
        this.balance = opening;
    }

    final boolean withdraw(final double amount) {   // final method, final parameter
        if (balance - amount < MIN_BALANCE) {
            return false;
        }
        balance -= amount;
        return true;
    }

    String summary() {
        return accountNo + " Rs " + balance;
    }
}

class SalaryAccount extends Account {
    SalaryAccount(String no, double opening) { super(no, opening); }
    // cannot override withdraw(): it is final
}

public class Main {
    public static void main(String[] args) {
        final Account acc = new SalaryAccount("SAL-2201", 5000);
        System.out.println(acc.withdraw(3500) + " " + acc.summary());
        System.out.println(acc.withdraw(1000) + " " + acc.summary());
        System.out.println("Minimum: " + Account.MIN_BALANCE);
    }
}
true SAL-2201 Rs 1500.0 false SAL-2201 Rs 1500.0 Minimum: 1000.0
  • static final double MIN_BALANCEOne constant shared by every account; can't be reassigned anywhere.
  • private final String accountNoBlank final: set once by the constructor, never changed afterwards.
  • final boolean withdraw(final double amount)Subclasses can't override the rule; the method can't reassign its parameter.
  • final Account accThe variable always refers to this account; the account's balance can still change.
8 Line by line

A final reference to a changing array

Watch the stack: marks keeps pointing at the same array the whole time, while the heap shows the array's contents changing.

Worked examples: real problems, solved and traced in memory

Three business problems where final protects something important. Run each solution, then step through it: watch final fields get their single value inside the constructor, static constants sit with the class, and a final reference keep pointing at an object whose contents change.

Example 1Banking · account opening

Final account numbers and a shared minimum

Problem statement

Each account gets a permanent account number when it is opened. Every account must also respect the same minimum balance of Rs 1,000. Open two accounts and check whether each meets the minimum.

Input
"SB-101" with Rs 5000, "SB-102" with Rs 800
Expected output
SB-101 true
SB-102 false

Approach

  1. static final int MIN = 1000: one constant for the class.
  2. final String no: a blank final, set once in the constructor.
  3. Create two accounts and compare each balance with MIN.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Inventory · warehouse

A final array whose stock still changes

Problem statement

A warehouse keeps stock counts in final int[] stock. A restock method adds units. Show that the counts change even though the variable is final, and explain what final actually protects.

Input
stock = {10, 5}; restock item 1 by 20, item 0 by 15
Expected output
[25, 25] total 50

Approach

  1. Declare final int[] stock in main.
  2. Pass it to restock, which writes into the same array.
  3. Print: the contents changed; the reference never did.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Payments · checkout

A final process() every gateway must follow

Problem statement

Every payment must be validated before it is charged. Subclasses may change how the fee is computed, but nobody may change the order of steps. Run a UPI payment of Rs 2,000 through the protected flow.

Input
UPI payment Rs 2000 (fee 0)
Expected output
Charged: Rs 2000

Approach

  1. Payment.process() is final: validate, then add the fee.
  2. fee() is not final, so Upi overrides it.
  3. Trace the final method calling the overridable one.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 7.8 in one screen

  • final variable: assigned exactly once (local, parameter, instance or static field).
  • Blank final field: no initializer; every constructor must set it once.
  • static final = constant, named in CAPITALS.
  • Effectively final: never reassigned; required for locals used in lambdas and inner classes.
  • final method: can't be overridden (still inherited). final class: can't be extended (String, Integer, Math).
  • A final reference can't point elsewhere, but the object it points to can still change.
  • abstract + final is illegal; final ≠ finally ≠ finalize().

Interview and viva questions

What is the difference between final, finally and finalize?

final is a modifier for variables, methods and classes. finally is a block after try/catch that always runs, used for cleanup. finalize() was a method on Object that the garbage collector might call before reclaiming an object; it's unreliable and has been deprecated since Java 9.

Does making a reference final make the object immutable?

No. final only stops the variable from being reassigned. An immutable object needs a design: private final fields, no setters, a final class, and defensive copies of mutable parts (Lesson 7.1). A final reference to an immutable object gives you both guarantees.

Why is the String class final?

String's immutability is relied on by the string pool, by hash-based collections (a key's hash must never change), by class loading and by security checks such as file paths and URLs. If String could be subclassed, a subclass could add mutable state or override methods and break all of those guarantees.

Can a final method be overloaded? Can a constructor be final?

Overloading is allowed: a method with the same name but different parameters is a different method. Constructors can't be final (or abstract or static); they're never inherited, so there's nothing to override and the modifier would be meaningless. javac rejects it.

Why must local variables used in a lambda be effectively final?

A lambda captures a copy of the local variable's value, and it may run later, even on another thread, after the method has returned. If the original could still change, the copy and the original could disagree. Requiring the variable to be final or effectively final avoids that confusion.

Practice problems

In each problem, decide what must never change after it is set, and let final make the compiler enforce it. Watch out for the difference between a final reference and an unchangeable object. The last three are business problems.

P1Easy

Constants for a fee calculator

Store the convenience fee percentage (2%) and the maximum fee (Rs 50) as static final constants and compute the fee for orders of Rs 800 and Rs 4,000.

Input
orders Rs 800 and Rs 4000
Expected output
Fee on 800: 16.0
Fee on 4000: 50.0
Show solution and explanation
public class Main {
    static final double FEE_PERCENT = 2.0;
    static final double MAX_FEE = 50;

    static double fee(double order) {
        return Math.min(order * FEE_PERCENT / 100, MAX_FEE);
    }

    public static void main(String[] args) {
        System.out.println("Fee on 800: " + fee(800));
        System.out.println("Fee on 4000: " + fee(4000));
    }
}
Fee on 800: 16.0 Fee on 4000: 50.0

How it works: 2% of 800 = 16; 2% of 4000 = 80, capped at 50. Constants are written in CAPITALS by convention. Any line like MAX_FEE = 100; would fail to compile.

P2Easy

Blank final set by the constructor

A Student has a final roll number set in the constructor and a changeable phone number. Create a student and update the phone.

Input
roll "CS-17", phone "98200 11111" then "98200 22222"
Expected output
CS-17 / 98200 11111
CS-17 / 98200 22222
Show solution and explanation
class Student {
    private final String roll;
    private String phone;

    Student(String roll, String phone) {
        this.roll = roll;
        this.phone = phone;
    }

    void setPhone(String phone) { this.phone = phone; }

    String info() { return roll + " / " + phone; }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student("CS-17", "98200 11111");
        System.out.println(s.info());
        s.setPhone("98200 22222");
        System.out.println(s.info());
    }
}
CS-17 / 98200 11111 CS-17 / 98200 22222

How it works: roll is a blank final: declared without a value, assigned exactly once in the constructor. Adding a setRoll method would not compile ("cannot assign a value to final variable roll").

P3Easy

final local chosen on two paths

Pick a delivery charge into a final int charge using if-else: Rs 0 for orders of Rs 499 and above, else Rs 40. Print it for Rs 350 and Rs 650.

Input
orders Rs 350 and Rs 650
Expected output
350 -> Rs 40
650 -> Rs 0
Show solution and explanation
public class Main {
    static int deliveryCharge(int order) {
        final int charge;
        if (order >= 499) {
            charge = 0;
        } else {
            charge = 40;
        }
        return charge;
    }

    public static void main(String[] args) {
        System.out.println("350 -> Rs " + deliveryCharge(350));
        System.out.println("650 -> Rs " + deliveryCharge(650));
    }
}
350 -> Rs 40 650 -> Rs 0

How it works: a final local may be assigned in different branches, as long as each path assigns it exactly once. javac's definite-assignment check proves that here.

P4Medium

final reference, changing array

A final int[] seats holds booked seats per row. Book 2 more seats in row 1 and print the array. Then explain (in a comment) why seats = new int[3] would not compile.

Input
seats = {10, 4, 7}; book 2 in row index 1
Expected output
[10, 6, 7]
Show solution and explanation
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        final int[] seats = {10, 4, 7};
        seats[1] += 2;                       // allowed: the array object changes
        // seats = new int[3];               // not allowed: the reference is final
        System.out.println(Arrays.toString(seats));
    }
}
[10, 6, 7]

How it works: final fixes which array seats refers to, not the numbers inside it. That's why seats[1] += 2 works, while pointing seats at a new array is a compile error.

P5Medium

Effectively final in a lambda

Use a local variable threshold (never reassigned) inside a lambda that checks whether an order amount is large. Test it with Rs 1,200 and Rs 12,000.

Input
threshold 10000; amounts 1200, 12000
Expected output
1200 large? false
12000 large? true
Show solution and explanation
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        int threshold = 10000;                 // effectively final
        Predicate<Integer> isLarge = amt -> amt > threshold;
        System.out.println("1200 large? " + isLarge.test(1200));
        System.out.println("12000 large? " + isLarge.test(12000));
    }
}
1200 large? false 12000 large? true

How it works: threshold isn't marked final, but it's never reassigned, so it's effectively final and the lambda may use it. Adding threshold = 5000; anywhere in the method would make the lambda line fail to compile. (Predicate is a functional interface from the JDK; Module 14.)

P6Medium

final method in a parent

Exam has a final method result(int marks) that returns PASS for 40 and above. PracticalExam extends it and adds describe(). Show that the child uses the parent's final rule.

Input
PracticalExam, marks 38 and 72
Expected output
Practical: 38 -> FAIL
Practical: 72 -> PASS
Show solution and explanation
class Exam {
    final String result(int marks) {
        return marks >= 40 ? "PASS" : "FAIL";
    }
}

class PracticalExam extends Exam {
    String describe(int marks) {
        return "Practical: " + marks + " -> " + result(marks);
    }
}

public class Main {
    public static void main(String[] args) {
        PracticalExam p = new PracticalExam();
        System.out.println(p.describe(38));
        System.out.println(p.describe(72));
    }
}
Practical: 38 -> FAIL Practical: 72 -> PASS

How it works: a final method is inherited and can be called, but not overridden, so every kind of exam uses the same pass rule. Writing String result(int marks) in PracticalExam would give "overridden method is final".

P7Medium

An immutable Money class

Write a final class Money with a private final long paise field, a plus method that returns a new Money, and a toString that shows rupees. Add Rs 199.50 and Rs 50.75.

Input
Rs 199.50 + Rs 50.75
Expected output
Rs 199.50 + Rs 50.75 = Rs 250.25
Show solution and explanation
final class Money {
    private final long paise;

    Money(long paise) {
        this.paise = paise;
    }

    Money plus(Money other) {
        return new Money(paise + other.paise);
    }

    @Override
    public String toString() {
        return String.format("Rs %d.%02d", paise / 100, paise % 100);
    }
}

public class Main {
    public static void main(String[] args) {
        Money a = new Money(19950);
        Money b = new Money(5075);
        Money total = a.plus(b);
        System.out.println(a + " + " + b + " = " + total);
    }
}
Rs 199.50 + Rs 50.75 = Rs 250.25

How it works: final class (no subclass can add setters), private final field (no reassignment), and plus returns a new object, so a and b never change. Storing paise as a long avoids floating-point rounding with money.

P8Business problem

Bank account with a final account number and minimum balance

Every account has a final account number, a constant minimum balance of Rs 2,000 for all accounts, and a final withdraw rule. Withdraw Rs 6,000 and then Rs 3,000 from an account opened with Rs 10,000.

Input
open Rs 10,000; withdraw 6000, then 3000
Expected output
Withdraw 6000: true -> SB-55021: Rs 4000.0
Withdraw 3000: false -> SB-55021: Rs 4000.0
Show solution and explanation
class BankAccount {
    static final double MIN_BALANCE = 2000;
    private final String accountNo;
    private double balance;

    BankAccount(String accountNo, double opening) {
        this.accountNo = accountNo;
        this.balance = opening;
    }

    final boolean withdraw(double amount) {
        if (amount <= 0 || balance - amount < MIN_BALANCE) return false;
        balance -= amount;
        return true;
    }

    String statement() {
        return accountNo + ": Rs " + balance;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount("SB-55021", 10000);
        System.out.println("Withdraw 6000: " + acc.withdraw(6000) + " -> " + acc.statement());
        System.out.println("Withdraw 3000: " + acc.withdraw(3000) + " -> " + acc.statement());
    }
}
Withdraw 6000: true -> SB-55021: Rs 4000.0 Withdraw 3000: false -> SB-55021: Rs 4000.0

How it works:

  1. The account number is a blank final; the minimum is a shared constant; the withdraw rule can't be overridden by any account subclass.
  2. 10,000 − 6,000 = 4,000 is fine; 4,000 − 3,000 = 1,000 is below 2,000, so it's refused.
  3. The balance itself is deliberately not final: it must change.
P9Business problem

Payroll run with final configuration

A payroll run uses fixed settings for the month: PF rate 12% of basic, professional tax Rs 200. Put them in a final configuration object with final fields, and compute net pay for three employees.

Input
basics: Anil 30000, Sara 45000, Kiran 80000
Expected output
Anil   net Rs 26,200.00
Sara   net Rs 39,400.00
Kiran  net Rs 70,200.00
Show solution and explanation
final class PayrollConfig {
    final double pfRate;
    final int professionalTax;

    PayrollConfig(double pfRate, int professionalTax) {
        this.pfRate = pfRate;
        this.professionalTax = professionalTax;
    }
}

public class Main {
    static double net(double basic, PayrollConfig cfg) {
        return basic - basic * cfg.pfRate - cfg.professionalTax;
    }

    public static void main(String[] args) {
        final PayrollConfig cfg = new PayrollConfig(0.12, 200);
        String[] names = {"Anil", "Sara", "Kiran"};
        double[] basics = {30000, 45000, 80000};
        for (int i = 0; i < names.length; i++) {
            System.out.printf("%-6s net Rs %,.2f%n", names[i], net(basics[i], cfg));
        }
    }
}
Anil net Rs 26,200.00 Sara net Rs 39,400.00 Kiran net Rs 70,200.00

How it works:

  1. Because the class is final and its fields are final, nothing during the run can change the rates halfway through.
  2. Anil: 30,000 − 3,600 − 200 = 26,200. Sara: 45,000 − 5,400 − 200 = 39,400. Kiran: 80,000 − 9,600 − 200 = 70,200.
  3. Next month's settings would be a new PayrollConfig object. (Rates here are simplified for teaching.)
P10Business problem

Delivery slots that can't be swapped out

A delivery hub keeps private final List<String> slots. Expose the slots safely so a caller can read them but cannot clear them, and show that trying to clear the returned list fails while the hub's slots stay intact.

Input
slots "9-11", "11-1", "4-6"
Expected output
Clear refused: the list is unmodifiable
Hub slots: [9-11, 11-1, 4-6]
Show solution and explanation
import java.util.ArrayList;
import java.util.List;

class DeliveryHub {
    private final List<String> slots = new ArrayList<>();

    DeliveryHub() {
        slots.add("9-11");
        slots.add("11-1");
        slots.add("4-6");
    }

    List<String> getSlots() {
        return List.copyOf(slots);
    }
}

public class Main {
    public static void main(String[] args) {
        DeliveryHub hub = new DeliveryHub();
        List<String> view = hub.getSlots();
        try {
            view.clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("Clear refused: the list is unmodifiable");
        }
        System.out.println("Hub slots: " + hub.getSlots());
    }
}
Clear refused: the list is unmodifiable Hub slots: [9-11, 11-1, 4-6]

How it works:

  1. final on the field guarantees the hub always uses the same list object.
  2. But final alone would still let callers clear() it through a getter that returns the field. List.copyOf (Java 10+) returns an unmodifiable copy, so clear() throws UnsupportedOperationException.
  3. Together: a fixed reference to data that only the hub can change.

Real-world scenario questions (asked in company interviews)

final turns "please don't change this" into a rule the compiler checks. Decide your answer first, then open the model answer.

Billing · constantsFresher

The tax rate that changed in production

A billing service has public static double GST_RATE = 0.18;. A new report feature sets GST_RATE = 0.12 to calculate a scenario, and for the next hour all invoices use 12%. What's the fix, and what else would you check?

The interviewer is checking: do you know how to make true constants?

Show a strong answer
  • Declare it public static final double GST_RATE = 0.18;. The report's assignment then fails to compile; the report should use a local variable for its scenario.
  • Search for other public static non-final fields: each one is shared mutable state that any code can change.
  • Since GST rates differ by product category and change by notification, consider loading rates from configuration or a rate table, keeping each loaded value immutable.

Avoid: making it private but adding a public setter. That just moves the problem.

Fintech · security1–3 years

A subclass that skipped the audit

A PaymentProcessor base class has process() that validates, charges and writes an audit record. A team building a new wallet subclass overrode process() for speed and skipped the audit. How do you prevent this?

The interviewer is checking: do you know when to make methods final?

Show a strong answer
  • Make process() final so it can't be overridden, and expose the varying part as an abstract or protected step such as charge() (template method, Lesson 7.6).
  • Make helper steps that must never change (audit()) private or final.
  • Add a test that checks an audit record exists for every processor type.

Avoid: a comment saying "please call super.process()". Comments aren't enforced.

E-commerce · code review1–3 years

"It's final, so it's safe"

A developer says the Cart class is safe because it has private final List<Item> items and a getter. A bug report shows that a discount module emptied a customer's cart by calling getItems().clear(). Explain to the developer what went wrong.

The interviewer is checking: do you understand final reference vs immutable object?

Show a strong answer
  • final means items always refers to the same list, but that list is still a normal, changeable ArrayList, and the getter hands it out.
  • Return List.copyOf(items) or Collections.unmodifiableList(items); changes then go only through Cart's methods (addItem, removeItem) which validate.
  • final is still useful here: it guarantees the cart never swaps to a different list, so the protection above can't be bypassed by reassignment.

Avoid: treating final as a synonym for immutable.

SaaS · concurrency3+ years

Configuration shared across threads

A multi-tenant service builds a TenantConfig object at start-up and shares it across hundreds of request threads. A teammate proposes adding setters so admins can change limits at run time. What design would you suggest, and where does final help?

The interviewer is checking: do you know that final fields help safe publication of immutable objects?

Show a strong answer
  • Keep TenantConfig immutable: a final class with private final fields and no setters. Java's memory model guarantees that other threads see final fields correctly initialised once the constructor finishes, without locks.
  • To change limits, build a new TenantConfig and swap a single shared reference atomically (for example AtomicReference, Module 12).
  • Each request reads the reference once, so it sees one consistent configuration.

Avoid: setters on a shared object, which give threads half-updated, inconsistent views.