skip to content

Why does declaring a variable inside versus outside a loop body matter, especially for lambdas/closures that capture it?

level: seniorimportance: should knowfreq 45%

answer

  1. Loop-body local = fresh per iteration
  2. Outside-loop var = one instance, mutated
  3. Closures capture only effectively-final locals
  4. Capture is by value (copy at creation)
  5. Copy index into per-iteration local to capture it

basics

~20 s

A variable declared inside the loop body is fresh on every iteration; one declared outside is shared across all iterations. This matters for lambdas, which can only capture variables that are effectively final — a fresh per-iteration variable can be captured, a shared mutating one cannot.

solid answer

~50 s

Each time control enters a block, its local variables are created anew, so a variable declared inside a loop body is a distinct instance per iteration. A variable declared outside the loop is a single instance mutated across iterations. This distinction drives lambda/anonymous-class capture: Java only lets a closure capture local variables that are effectively final — assigned exactly once and never reassigned. A per-iteration variable declared in the loop body is effectively final within that iteration, so each lambda captures its own value; you see this with the enhanced for-loop variable too. A loop counter declared outside (or the index of a classic for) is reassigned every iteration, so it is not effectively final and cannot be captured directly — you must copy it into a fresh per-iteration local. Understanding this prevents the classic bug where every captured lambda ends up seeing the loop's final value.

code

java · 10 lines
java
List<Runnable> tasks = new ArrayList<>();
for (int i = 0; i < 3; i++) {
    int captured = i;                 // fresh per iteration -> effectively final
    tasks.add(() -> System.out.println(captured));
}
tasks.forEach(Runnable::run);         // prints 0, 1, 2

// for (int i = 0; i < 3; i++) {
//     tasks.add(() -> System.out.println(i)); // ERROR: i is not effectively final
// }

go deeper

for a junior

Knows variables inside a loop are recreated each pass and ones outside are shared; may not yet connect this to lambdas.

for a middle

Understands effectively-final and that a lambda cannot capture a reassigned loop counter; can apply the per-iteration copy fix.

for a senior

Explains capture-by-value, the per-entry-instantiation rule, why enhanced-for variables capture cleanly, and the classic loop-capture bug.

for a principal

Reasons about the design trade-off (value capture + effectively-final vs. reference capture), contrasts with other languages' closure semantics, and guides team conventions to avoid capture-related concurrency bugs.

## Per-entry instantiation A core rule of block scope: **every time control enters a block, a fresh set of that block's local variables is created**, and they are discarded when the block exits. A loop body is a block, so its locals are recreated on each iteration. ```java for (int i = 0; i < 3; i++) { int squared = i * i; // a NEW 'squared' each iteration } ``` Contrast a variable declared **outside** the loop, which is one variable reused (mutated) across all iterations: ```java int sum = 0; // one variable for (int i = 0; i < 3; i++) { sum += i; // same 'sum' mutated each time } ``` ## What 'effectively final' means A local variable (or parameter) is **effectively final** if it is assigned exactly once and never reassigned afterward — i.e. you *could* add the `final` keyword without a compile error. Java introduced this concept so lambdas and anonymous classes don't require you to literally write `final` everywhere. ## Why closures can only capture effectively-final locals A **closure** (a lambda or anonymous inner class) can refer to local variables of the enclosing method/block — this is **capture**. Java captures by **value**: it copies the variable's value into the closure at the moment the closure is created. If the original could keep changing, the copy and the original would diverge confusingly, so Java requires captured locals to be effectively final. (Fields don't have this restriction because they are captured via the enclosing object reference, not by value.) ## The loop-capture bug and how scope fixes it Because a loop body's local is a fresh instance per iteration, it is effectively final *within that iteration* and can be captured cleanly: ```java List<Runnable> tasks = new ArrayList<>(); for (int i = 0; i < 3; i++) { int captured = i; // fresh per iteration -> effectively final tasks.add(() -> System.out.println(captured)); } // prints 0, 1, 2 — each lambda has its own 'captured' ``` But you **cannot** capture the loop index `i` of a classic `for` directly: ```java for (int i = 0; i < 3; i++) { tasks.add(() -> System.out.println(i)); // COMPILE ERROR: i is not effectively final } ``` `i` is reassigned every iteration (`i++`), so it is not effectively final. The fix is exactly the per-iteration copy above. (In languages without per-iteration freshness, such as older JavaScript with `var`, all the closures would have shared one `i` and printed `3, 3, 3` — Java's scope rules plus the effectively-final requirement prevent that footgun.) ## The enhanced for-loop is friendly here The enhanced for-loop variable is conceptually a fresh local each iteration, so it can be captured directly: ```java for (String name : names) { tasks.add(() -> System.out.println(name)); // fine: 'name' is effectively final per iteration } ``` ## Practical guidance - Declare variables in the **narrowest** block that needs them. - If a lambda inside a loop must capture an index, copy it into a per-iteration local first. - Prefer enhanced for / streams where possible; their loop variables capture cleanly. - Remember: scope governs visibility; the per-entry-instantiation rule governs which *instance* you get and therefore what a closure captures.

  • Why are fields not subject to the effectively-final rule for capture?
    Lambdas capture fields through the enclosing object's reference (this), not by copying a value, so the field can keep changing and the lambda sees the latest value.
  • How do you capture a classic for-loop's index in a lambda?
    Copy it into a fresh local declared inside the loop body (e.g. int idx = i;) and capture that; the per-iteration local is effectively final.

saying these in an interview costs you the question

  • Claiming Java captures local variables by reference
  • Trying to capture a classic for-loop's i directly in a lambda
  • Thinking a variable declared outside a loop is recreated each iteration
  • Confusing 'final' the keyword with 'effectively final' (the latter just means never reassigned)
  • Assuming all captured lambdas in a loop see the loop's last value (true in old JS var, not in correctly-scoped Java)

context