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.
Sir, it was meant to be a constant. How did one line in a test method change it for everyone?
Because nothing told Java it was a constant. Add final: static final double GST_RATE = 0.18;. Then that line won't even compile.
Is final only for constants?
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.
What does final mean?
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.
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
static final field, named in CAPITALS: GST_RATE.String.final at a glance
| Local variable | Assign once: final int x = 5; x = 6; → cannot assign a value to final variable x |
|---|---|
| Instance field | Set at declaration, in an initializer block, or in every constructor. Missing → variable rate might not have been initialized |
| Static field | Set at declaration or in a static block. Together, static final = a class constant. |
| Parameter | final double amount: final parameter amount may not be assigned |
| Method | Can't be overridden: overridden method is final. Still inherited and callable. |
| Class | Can'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.
Three places to put final
final variable
Assign once. Constants, IDs, values fixed for an object's life.
write oncefinal method
Children inherit it but can't override it: mandatory steps stay mandatory.
no overridingfinal class
No subclasses at all. String, Integer, Math are final.
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]);
}
}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 change | Impossible: no method changes the state |
| What's protected | Which object the variable points to | The object's data itself |
| How to get both | A 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).
How javac enforces final
A final local is assigned once
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").final double rate; if (isSenior) rate = 7.5; else rate = 7.0; // OK: exactly one assignment on each pathA blank final field is set in every constructor
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".private final String accountNo; Account(String no) { accountNo = no; } Account() { this("SB-NEW"); } // delegates, so still assigned onceEffectively final for lambdas and inner classes
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. Addint limit = 5000; // never reassigned → effectively final Runnable r = () -> System.out.println(limit);limit++anywhere and javac reports "must be final or effectively final".final methods lock the steps
Subclasses still inherit and callfinal String process() { return validate() + " → " + charge() + " → receipt"; }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).final classes close the hierarchy
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.class SafeString extends String { } // ✗ cannot inherit from final String
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 local: the value is fixed after the first assignment. The second line doesn't compile.
- Blank final field:
accountNohas no value at declaration; the constructor sets it once, and it's read-only afterwards. - Forgotten assignment: a constructor that leaves a blank final unset is a compile error.
- final reference:
markscan't point elsewhere, but the array object it points to is still mutable. - Reassign the reference: this is exactly what
finalforbids. - final class: String is final, so no class can extend it.
- For a final local, javac checks there is exactly one assignment. A final primitive initialised with a constant expression is a compile-time constant, which javac may inline wherever it's used.
- The blank final field lives in object #1 on the heap. The constructor gives it its only value.
- javac traces every path through each constructor. If one path can finish without setting the field, compilation fails.
finalonmarksfixes the arrow from the stack to array #2. The elements inside #2 are ordinary, changeable slots.- Assigning a new array would mean a new arrow. That's the one thing a final reference forbids.
- The class file of String is marked final; javac refuses any
extends String, and the JVM would refuse to load such a class.
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.
- Constant:
static final double PF = 0.12. Every payslip uses 12%: Rs 30,000 − 3,600 = Rs 26,400. - Locked value: a test that writes
PF = 0.10doesn't compile, so a stray line can't change everyone's PF. - final reference:
holidaysis final, yetadd("Diwali")works: the list object itself is ordinary and changeable. - What final blocks: pointing
holidaysat a different list is a compile error. - Protecting contents: the getter hands out
List.copyOf(holidays); trying to add to that copy throwsUnsupportedOperationException. - final method:
run()is final in Payroll, so a FastPayroll subclass can't override it to skip PF or audit steps.
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.
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.
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.
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.
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.
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);
}
}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.
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.
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.
"SB-101" with Rs 5000, "SB-102" with Rs 800
SB-101 true SB-102 false
Approach
static final int MIN = 1000: one constant for the class.final String no: a blank final, set once in the constructor.- 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.
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.
stock = {10, 5}; restock item 1 by 20, item 0 by 15[25, 25] total 50
Approach
- Declare
final int[] stockin main. - Pass it to
restock, which writes into the same array. - 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.
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.
UPI payment Rs 2000 (fee 0)
Charged: Rs 2000
Approach
Payment.process()isfinal: validate, then add the fee.fee()is not final, soUpioverrides it.- 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.
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.
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.
orders Rs 800 and Rs 4000
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));
}
}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.
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.
roll "CS-17", phone "98200 11111" then "98200 22222"
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());
}
}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").
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.
orders Rs 350 and Rs 650
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));
}
}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.
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.
seats = {10, 4, 7}; book 2 in row index 1[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));
}
}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.
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.
threshold 10000; amounts 1200, 12000
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));
}
}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.)
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.
PracticalExam, marks 38 and 72
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));
}
}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".
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.
Rs 199.50 + Rs 50.75
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);
}
}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.
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.
open Rs 10,000; withdraw 6000, then 3000
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());
}
}How it works:
- The account number is a blank final; the minimum is a shared constant; the withdraw rule can't be overridden by any account subclass.
- 10,000 − 6,000 = 4,000 is fine; 4,000 − 3,000 = 1,000 is below 2,000, so it's refused.
- The balance itself is deliberately not final: it must change.
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.
basics: Anil 30000, Sara 45000, Kiran 80000
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));
}
}
}How it works:
- Because the class is final and its fields are final, nothing during the run can change the rates halfway through.
- Anil: 30,000 − 3,600 − 200 = 26,200. Sara: 45,000 − 5,400 − 200 = 39,400. Kiran: 80,000 − 9,600 − 200 = 70,200.
- Next month's settings would be a new PayrollConfig object. (Rates here are simplified for teaching.)
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.
slots "9-11", "11-1", "4-6"
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());
}
}How it works:
finalon the field guarantees the hub always uses the same list object.- But final alone would still let callers
clear()it through a getter that returns the field.List.copyOf(Java 10+) returns an unmodifiable copy, soclear()throwsUnsupportedOperationException. - 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.
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 staticnon-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.
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()finalso it can't be overridden, and expose the varying part as an abstract or protected step such ascharge()(template method, Lesson 7.6). - Make helper steps that must never change (
audit())privateorfinal. - 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.
"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
finalmeansitemsalways refers to the same list, but that list is still a normal, changeable ArrayList, and the getter hands it out.- Return
List.copyOf(items)orCollections.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.
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
TenantConfigimmutable: a final class withprivate finalfields 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.