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;
publicclassFinalVariableDemo {
// static final = a true compile-time constant shared across all instances// Convention: ALL_CAPS with underscores for static final constantsprivatestaticfinalint MAX_LOGIN_ATTEMPTS = 3;
// instance final field — must be set in every constructorprivatefinalString username;
publicFinalVariableDemo(String username) {
// This is the ONE allowed assignment for this instance fieldthis.username = username;
}
publicvoiddemonstrateFinalBehaviour() {
// final local variable — assigned once, never changedfinaldouble 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 thisSystem.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 lockedfinalList<String> sessionTokens = newArrayList<>();
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 ERRORSystem.out.println("Session tokens: " + sessionTokens);
}
publicstaticvoidmain(String[] args) {
FinalVariableDemo demo = newFinalVariableDemo("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 workflowclassPaymentProcessor {
privatestaticfinaldouble 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.publicfinalbooleanprocessPayment(String customerId, double amount) {
// Step 1: fraud check — no subclass can skip or alter thisif (!passesFraudCheck(customerId, amount)) {
System.out.println("[BLOCKED] Fraud check failed for customer: " + customerId);
returnfalse;
}
// Step 2: delegate to subclass-specific charge logicboolean charged = chargeCustomer(customerId, amount);
// Step 3: audit log — no subclass can skip this eitherauditLog(customerId, amount, charged);
return charged;
}
// private fraud check — already hidden from subclassesprivatebooleanpassesFraudCheck(String customerId, double amount) {
// Simplified: flag anything over the thresholdreturn amount <= FRAUD_THRESHOLD;
}
// Subclasses override THIS to provide their own charging mechanismprotectedbooleanchargeCustomer(String customerId, double amount) {
System.out.println("[DEFAULT] Charging $" + amount + " to customer: " + customerId);
returntrue;
}
privatevoidauditLog(String customerId, double amount, boolean success) {
System.out.println("[AUDIT] Customer: " + customerId
+ " | Amount: $" + amount
+ " | Success: " + success);
}
}
// Subclass provides a Stripe-specific charge implementationclassStripePaymentProcessorextendsPaymentProcessor {
@OverrideprotectedbooleanchargeCustomer(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) { ... }
}
publicclassPaymentProcessor {
publicstaticvoidmain(String[] args) {
PaymentProcessor stripe = newStripePaymentProcessor();
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
[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.publicfinalclassMoney {
private final String currencyCode; // e.g. "USD", "EUR"
private final long amountInCents; // store as cents to avoid floating-point errorspublicMoney(String currencyCode, long amountInCents) {
if (currencyCode == null || currencyCode.isBlank()) {
thrownewIllegalArgumentException("Currency code must not be blank");
}
if (amountInCents < 0) {
thrownewIllegalArgumentException("Amount must not be negative");
}
// Both fields are final — assigned here and never changedthis.currencyCode = currencyCode.toUpperCase();
this.amountInCents = amountInCents;
}
// Returns a NEW Money object — the original is never mutatedpublicMoneyadd(Money other) {
if (!this.currencyCode.equals(other.currencyCode)) {
thrownewIllegalArgumentException(
"Cannot add " + this.currencyCode + " and " + other.currencyCode);
}
returnnewMoney(this.currencyCode, this.amountInCents + other.amountInCents);
}
publicStringgetFormattedAmount() {
// Divide by 100 to display as decimal dollars/euros/etc.return currencyCode + " " + String.format("%.2f", amountInCents / 100.0);
}
@Overridepublicbooleanequals(Object obj) {
if (this == obj) returntrue;
if (!(obj instanceofMoney)) returnfalse;
Money other = (Money) obj;
// Because the class is final, we KNOW obj is exactly Money — not a sneaky subclassreturnthis.amountInCents == other.amountInCents
&& this.currencyCode.equals(other.currencyCode);
}
@OverridepublicinthashCode() {
return31 * currencyCode.hashCode() + Long.hashCode(amountInCents);
}
@OverridepublicStringtoString() {
returngetFormattedAmount();
}
// --- Attempting to extend Money would cause a compile error: ---// class TaxedMoney extends Money { } <-- COMPILE ERROR: cannot inherit from final Moneypublicstaticvoidmain(String[] args) {
Money productPrice = new Money("USD", 1999); // $19.99Money shippingCost = new Money("USD", 599); // $5.99Money 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 = newMoney("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;
publicclassOrderCalculator {
// final parameters — the references 'items' and 'discountPercent' cannot be// reassigned inside this method. Prevents accidental shadowing in long methods.publicdoublecalculateTotal(finalList<String> items, finaldouble 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.99double 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 capturepublicvoidprintItemsAsync(finalList<String> items) {
// 'items' is final — safe to capture in the lambda belowRunnable printer = () -> {
for (String item : items) {
System.out.println(" - " + item);
}
};
printer.run();
}
publicstaticvoidmain(String[] args) {
OrderCalculator calculator = newOrderCalculator();
List<String> cart = List.of("Java Book", "Mechanical Keyboard", "USB Hub");
System.out.println("=== Order Summary ===");
calculator.calculateTotal(cart, 10.0); // 10% discountSystem.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.
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
If you cannot change the production code, add mockito-inline to the test classpath.
Comparison: final Variable, Method, and Class
Aspect
final Variable
final Method
final Class
What it prevents
Reassigning the variable reference
Subclasses overriding the method
Any class extending this class
Compile error trigger
Second assignment to the variable
@Override in a subclass
extends FinalClassName
Runtime cost
None — compile-time only
Enables JIT inlining (minor speedup)
None — compile-time only
Applies to objects?
Locks reference, NOT object contents
N/A
N/A
Common real-world use
Constants, immutable fields
Template Method pattern, security
Value types, utility classes
Java stdlib examples
Math.PI, Integer.MAX_VALUE
Object.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).
Q02 of 03SENIOR
Why is the String class declared final in Java? What security or correctness problems would arise if it weren't?
ANSWER
String is final to ensure: (1) Security — class loaders rely on immutable strings; a mutable subclass could bypass security checks. (2) Thread safety — immutable objects are inherently thread-safe. (3) Performance — String interning and caching hashCode only work if the value never changes. If String were not final, a subclass could override equals/hashCode and break every HashMap or Set that holds strings.
Q03 of 03SENIOR
If a class is not final but all of its constructors are private, can it be subclassed? How does this compare to making the class explicitly final — and when would you choose one approach over the other?
ANSWER
A class with only private constructors cannot be subclassed directly because subclasses require one of their constructors to call a super() constructor, which must be accessible. However, an inner class defined within the same file can still extend it. Compare to final: final prevents ANY subclassing, even from inner classes. Use private constructors when you want to control instantiation (e.g., singleton pattern) but still allow subclassing within the same class file. Use final when you want to prevent inheritance entirely.
01
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?
JUNIOR
02
Why is the String class declared final in Java? What security or correctness problems would arise if it weren't?
SENIOR
03
If a class is not final but all of its constructors are private, can it be subclassed? How does this compare to making the class explicitly final — and when would you choose one approach over the other?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Can a final variable be declared without being initialised immediately?
Yes — this is called a blank final variable. For instance fields, the assignment must happen in every constructor. For static fields, it must happen in a static initialiser block. For local variables, you must assign before the first use. The compiler tracks every code path and will refuse to compile if any path leaves a final variable unassigned.
Was this helpful?
02
What is the difference between final and effectively final in Java?
A final variable is explicitly declared with the final keyword and the compiler enforces that it's only assigned once. An effectively final variable has no final keyword but is never reassigned after its initial assignment — the compiler recognises it as safe to capture in lambdas and anonymous inner classes. The runtime behaviour is identical; the difference is only whether you write the keyword or not.
Was this helpful?
03
Does making a method final improve performance in Java?
Potentially, yes, but in modern Java the JIT compiler is smart enough to devirtualise and inline method calls even without the final keyword when it can prove at runtime that there's only one implementation. So final for performance is rarely necessary today. The real reasons to use final on methods are design clarity and security — preventing subclasses from changing behaviour you need to rely on.
Was this helpful?
04
Can you mock a final class with Mockito?
Yes, starting from Mockito 2.x you can mock final classes by configuring the mock-maker-inline extension. Add the dependency 'org.mockito:mockito-inline' and create a file 'mockito-extensions/org.mockito.plugins.MockMaker' in test resources containing 'mock-maker-inline'. However, a better design is to depend on an interface instead of the concrete final class.