Definition
- Effectively final – a local variable is effectively final if it is assigned exactly once and never reassigned; from Java 8 the compiler infers this without requiring the
finalkeyword. - Capture by copy – the compiler copies the local variable's value into a hidden field of the anonymous instance, because the instance can outlive the method's stack frame.
- Consistency guarantee – forbidding reassignment prevents two independent storage locations (stack local and captured copy) from diverging.
- Instance fields exempt – fields of the enclosing class live on the heap and are shared by reference, not copied, so they are not subject to this rule.
- Mutable-holder workaround – capture a final reference to a mutable object such as
AtomicInteger,AtomicReference, or a one-element array to mutate state safely.
Imagine Rajesh fills a transfer form at National Bank of India and hands a photocopy of the amount to a temporary clerk who will process it later, possibly after Rajesh has left the branch. If Rajesh were allowed to change the amount on his original form afterwards, the clerk's photocopy and Rajesh's form would disagree. So the bank's rule is: once you hand the amount to the clerk, you cannot change it. The amount is effectively frozen.
Understanding the Concepts
What does effectively final mean?
A local variable is effectively final if it is assigned exactly once and never reassigned afterward. Before Java 8 you had to write final explicitly. From Java 8 the compiler infers it, so you may omit final as long as you never reassign the variable.
package com.bank.service;
import java.math.BigDecimal;
public class EffectivelyFinalDemo {
public void process() {
BigDecimal fee = new BigDecimal("250.00"); // effectively final
// fee = new BigDecimal("300.00"); // uncommenting makes it NOT effectively final
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Processing fee: " + fee + " INR");
}
};
task.run();
}
public static void main(String[] args) {
new EffectivelyFinalDemo().process();
}
}
Processing fee: 250.00 INR
How does capture by copy work internally?
When an anonymous class refers to a local variable, the compiler copies that value into a hidden field of the anonymous instance. This is necessary because the anonymous object can outlive the method. Once the method returns, its stack frame is destroyed, but the anonymous object may still be referenced and executed later.
package com.bank.service;
import java.math.BigDecimal;
public class CaptureDemo {
public Runnable createTask() {
BigDecimal transferAmount = new BigDecimal("100000"); // on stack
// The compiler copies transferAmount into the anonymous class's hidden field
return new Runnable() {
@Override
public void run() {
// Uses the captured copy, not the stack local
System.out.println("Deferred transfer: " + transferAmount + " INR");
}
};
// After this method returns, the stack local is gone
// but the anonymous object still has its copy
}
public static void main(String[] args) {
Runnable task = new CaptureDemo().createTask();
task.run(); // works because the copy survives
}
}
Deferred transfer: 100000 INR
Why does Java require effectively final?
Because the anonymous object holds a copy, allowing reassignment of the local would create two independent storage locations for the same variable: the stack local and the captured copy. Changing one would not change the other, so the programmer could not reason about which value the anonymous class sees. Java chose to forbid reassignment rather than silently maintain two diverging values.
package com.bank.service;
import java.math.BigDecimal;
public class WhyEffectivelyFinal {
public void brokenExample() {
BigDecimal rate = new BigDecimal("0.08");
Runnable task = new Runnable() {
@Override
public void run() {
// System.out.println("Rate: " + rate); // would use the OLD value
}
};
// rate = new BigDecimal("0.09"); // COMPILE ERROR:
// "local variables referenced from an inner class must be final or effectively final"
}
}
What happens when you violate the rule?
The compiler rejects the code with a clear error message. Both the reassignment and the capture site are flagged.
package com.bank.service;
import java.math.BigDecimal;
public class ViolationDemo {
public void processTransfers() {
BigDecimal total = BigDecimal.ZERO;
Runnable task = new Runnable() {
@Override
public void run() {
// COMPILE ERROR: total is not effectively final
// System.out.println("Total: " + total);
}
};
total = new BigDecimal("50000"); // this reassignment breaks effectively-final
}
}
What is the mutable-holder workaround?
Capture an effectively-final reference to a mutable object. The reference itself never changes, so it stays effectively final, but the object's contents can be updated. Both the anonymous class and the calling code share the same heap object.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;
public class MutableHolderDemo {
public void batchProcess() {
// The reference 'total' is effectively final
// But its contents can be mutated
AtomicReference<BigDecimal> total = new AtomicReference<>(BigDecimal.ZERO);
Runnable task1 = new Runnable() {
@Override
public void run() {
total.updateAndGet(t -> t.add(new BigDecimal("75000")));
}
};
Runnable task2 = new Runnable() {
@Override
public void run() {
total.updateAndGet(t -> t.add(new BigDecimal("125000")));
}
};
task1.run();
task2.run();
System.out.println("Batch total: " + total.get() + " INR");
}
public static void main(String[] args) {
new MutableHolderDemo().batchProcess();
}
}
Batch total: 200000 INR
How is this different from instance fields?
Instance fields and static fields are not subject to the effectively-final restriction because they live on the heap and are accessed through a reference to the enclosing object, not copied. The anonymous class accesses them via Outer.this, which is a shared reference, not a snapshot.
package com.bank.service;
import java.math.BigDecimal;
public class FieldVsLocal {
private BigDecimal balance = new BigDecimal("500000"); // instance field
public void updateBalance() {
Runnable task = new Runnable() {
@Override
public void run() {
// Instance field: no effectively-final restriction
balance = balance.add(new BigDecimal("10000"));
System.out.println("Updated balance: " + balance + " INR");
}
};
task.run();
}
public static void main(String[] args) {
new FieldVsLocal().updateBalance();
}
}
Updated balance: 510000 INR
What about thread safety when using mutable holders?
Capture by copy gives you a stable snapshot that is inherently safe to pass to another thread. The mutable-holder workaround opts out of that safety, so you must handle thread-safety yourself. Use AtomicInteger or AtomicReference instead of a plain array for multi-threaded banking jobs.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeMutableHolder {
public void countSuccessfulTransfers() throws InterruptedException {
AtomicInteger successCount = new AtomicInteger(0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// Simulate Priya's transfer succeeding
successCount.incrementAndGet();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// Simulate Rajesh's transfer succeeding
successCount.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Successful transfers: " + successCount.get());
}
public static void main(String[] args) throws InterruptedException {
new ThreadSafeMutableHolder().countSuccessfulTransfers();
}
}
Successful transfers: 2
Does the same rule apply to lambdas?
Yes. Both anonymous classes and lambdas capture local variables in the same way (by copy), so the effectively-final rule applies identically to both. The mechanism is the same; only the syntax differs.
package com.bank.service;
import java.math.BigDecimal;
public class LambdaSameRule {
public void demo() {
BigDecimal rate = new BigDecimal("0.05"); // effectively final
// Lambda captures the same way as anonymous class
Runnable lambda = () -> System.out.println("Interest rate: " + rate);
lambda.run();
// rate = new BigDecimal("0.06"); // COMPILE ERROR for lambda too
}
public static void main(String[] args) {
new LambdaSameRule().demo();
}
}
Interest rate: 0.05
Visual Overview
┌─────────────────────────────────────────────┐ │ Effectively Final: Capture by Copy │ ├─────────────────────────────────────────────┤ │ │ │ Method Stack Frame │ │ ┌───────────────────┐ │ │ │ BigDecimal fee │──── value: 250.00 │ │ └────────┬──────────┘ │ │ │ copy at creation time │ │ ▼ │ │ Anonymous Instance (Heap) │ │ ┌───────────────────┐ │ │ │ hidden$fee │──── value: 250.00 │ │ └───────────────────┘ │ │ │ │ Method returns → stack frame destroyed │ │ Anonymous object survives with its copy │ │ │ ├─────────────────────────────────────────────┤ │ Mutable Holder Workaround │ │ │ │ AtomicReference<BigDecimal> total │ │ ┌───────────────────┐ │ │ │ reference (final) │──→ heap object │ │ └───────────────────┘ ┌──────────┐ │ │ │ value: │ │ │ Anonymous class ─────────┤ mutable │ │ │ (shares same ref) └──────────┘ │ │ │ └─────────────────────────────────────────────┘
Key Takeaway
An anonymous class can access local variables of the enclosing method, but only if those variables are final or effectively final, meaning they are assigned exactly once and never reassigned. The same rule applies identically to lambdas, because both capture local variables in the same way: by copy.
The reason for the restriction is capture by copy. When an anonymous class refers to a local variable, the compiler copies that value into a hidden field of the anonymous instance. This is necessary because the anonymous object can outlive the method that created it. Once the method returns, its stack frame is destroyed, but the object carries its own copy. If Java allowed reassignment of the local, the stack copy and the captured copy would diverge, and nobody could reason about which value the anonymous class sees.
In a banking project at National Bank of India, Rajesh builds anonymous callbacks for batch transfer processing. Each transfer's reference and amount are effectively final, so each callback safely captures its own stable snapshot. The capture-by-copy behaviour is actually a benefit here, because the snapshot is safe to run on another thread without a data race.
When you genuinely need mutable shared state, use the standard workaround of a final reference to a mutable holder such as AtomicReference. The reference itself never changes, so it stays effectively final, but the value inside it can be updated, and every callback sees the update because they share the same heap object.
Remember one sentence: an anonymous class captures a copy of local variables, so those variables must be effectively final to prevent two diverging copies, and the clean answer to needing mutation is a shared atomic holder.
---