What does it mean that a local or anonymous class can only capture effectively-final local variables, and why does Java impose this rule?
answer
- Inner object outlives the stack frame
- Capture = copy the value into a synthetic field
- Reassignment would let copies drift -> forbidden
- Effectively final = assigned once, never reassigned
- Workarounds: array box, Atomic, enclosing field
basics
~20 sA local variable used inside a local/anonymous class must never be reassigned after it's set — that's what 'effectively final' means. Java requires this because the class captures a copy of the variable's value, so allowing reassignment would let the two copies drift apart.
solid answer
~50 sLocal and anonymous classes can outlive the method that created them — for example, an anonymous Runnable handed to another thread runs after the method returns and its stack frame is gone. The class therefore can't reference the live stack slot of a local variable; instead the compiler captures the variable's value into a synthetic field of the inner-class instance. If the local were allowed to change after capture, you'd have two independent copies (the method's and the captured one) that could disagree, which is confusing and error-prone. To avoid this, Java requires captured locals to be final or effectively final — assigned exactly once and never reassigned. 'Effectively final' (since Java 8) means you don't have to write the `final` keyword; the compiler infers it if the variable is in fact never reassigned. To capture mutable state, wrap it: use a one-element array, an AtomicReference, or a field of the enclosing object, which IS captured by reference (via the enclosing-instance pointer).
code
java · 16 linesRunnable makeCounterPrinter() {
// Won't compile if we try: int n = 0; ... n++ then capture n.
int[] box = {0}; // reference is effectively final
return new Runnable() {
@Override public void run() {
box[0]++; // mutate the array's contents, allowed
System.out.println(box[0]);
}
};
}
void bad() {
int x = 0;
x = 5; // reassigned -> not effectively final
Runnable r = () -> System.out.println(x); // COMPILE ERROR
}go deeper
Knows captured locals must be effectively final and that you can't reassign them after use in the inner class.
Explains value-capture into a synthetic field, why a vanished stack frame forces it, and the array/Atomic/field workarounds.
Distinguishes capturing the variable's binding vs its value, contrasts with closures in other languages, and reasons about thread-safety of captured mutable state.
Weighs API/concurrency design around captured state, prefers immutable capture for thread-safety, and guides teams away from mutable-box hacks toward cleaner designs (passing state explicitly, atomics).
## Setup: what 'capture' means When a local class or anonymous class refers to a local variable (or method parameter) from the method it's declared in, that is called **capturing** the variable. Example: ```java Runnable makeTask(String name) { int id = 42; return new Runnable() { @Override public void run() { System.out.println(name + " " + id); // captures `name` and `id` } }; } ``` Here the returned `Runnable` may be run much later, after `makeTask` has already returned. By then the method's **stack frame** — the chunk of memory holding `id`, `name`, and other locals — has been destroyed. So the object can't point at those stack slots. ## How Java actually captures The compiler solves this by **copying the value** of each captured local into a hidden (synthetic) field of the generated inner-class instance at the moment the instance is created. The inner object then reads its own copy, which survives on the heap as long as the object does. Primitives are copied by value; object references are copied (the *reference* is copied, so both point at the same heap object). ## The rule: effectively final Because the inner object holds an independent copy, Java forbids the original local from being **reassigned** anywhere in the method. A variable that is assigned once and never reassigned is **effectively final** — it behaves as if you wrote `final`, even though you didn't. Before Java 8 you had to spell out `final`; since Java 8 the compiler infers it, and you only get a compile error if you actually do reassign. Why the restriction? If reassignment were allowed, the method's copy and the captured copy could **drift apart**, and it would be ambiguous which value the inner class 'should' see — the one at capture time or the latest one. Rather than pick a confusing semantics, Java rules it out at compile time. (Some languages like JavaScript capture the variable *binding* and let it mutate; Java deliberately chose value capture for simplicity and thread-safety.) ## What counts as a reassignment ```java int x = 1; x = 2; // reassignment -> x is NOT effectively final Runnable r = () -> System.out.println(x); // compile error ``` Mutating the *object* a final reference points to is fine — it's the *variable* that must not be reassigned: ```java List<String> list = new ArrayList<>(); // effectively final reference Runnable r = () -> list.add("ok"); // OK: we mutate the object, not the variable ``` ## Working around it when you truly need mutable captured state Three idioms: 1. **One-element array** — `int[] box = {0};` then `box[0]++` inside; the *reference* `box` stays effectively final while the contents change. 2. **Atomic types** — `AtomicInteger count = new AtomicInteger();` then `count.incrementAndGet()`; also thread-safe. 3. **A field of the enclosing object** — instance and static fields are *not* captured by value. The inner class reaches them through the captured **enclosing-instance reference** (for an instance field) or directly (for a static field), so writes are visible and allowed. ## Note on fields vs locals The effectively-final rule applies **only to local variables and parameters**, not to fields. That's because a field lives on the heap (inside the enclosing object), and the inner class accesses it live through the enclosing-instance pointer — there is no copying and therefore no drift problem.
- Why are fields exempt from the effectively-final rule but local variables are not?Fields live on the heap inside the enclosing object and are reached live through the captured enclosing-instance reference, so there is no copied snapshot to drift. Locals live on the stack, which disappears when the method returns, so the inner class must capture a copy — hence they must not be reassigned.
- How can you increment a counter from inside an anonymous Runnable that captures it?Wrap it: use a one-element array (`int[] box = {0}; box[0]++`), an `AtomicInteger`, or make it a field of the enclosing class. The variable itself stays effectively final while the contents/object mutate.
saying these in an interview costs you the question
- Claiming you must write the `final` keyword (effectively final is inferred since Java 8)
- Saying you can never mutate captured state (you can mutate the object a final reference points to, or use a box/field)
- Thinking fields must also be effectively final (the rule is only for locals/params)
- Believing the inner class reads the live stack variable rather than a captured copy