λ Lambdas & Functional Java · Intermediate

Effectively final capture in Java

Lambdas can only capture locals that are never reassigned.

🧩 The mysteryint count = 0; list.forEach(s -> count++); looks perfectly innocent. The compiler refuses it outright. Yet change ONE thing and it compiles fine.

Lambdas take a snapshot

A lambda can use local variables from the surrounding method, but it captures a copy of the value. So Java only allows locals that are final or effectively final: never reassigned after initialization. The final keyword itself is optional.

String hi = "Hi ";   // never reassigned
names.forEach(n -> IO.println(hi + n)); // ✓
🔮 Predict it

Your turn

What happens?

int sum = 0;
List.of(1, 2, 3).forEach(n -> sum += n);
System.out.println(sum);
  1. Prints 6
  2. Prints 0
  3. Compile error
Show the answer

sum += n would modify a captured local, so sum isn't effectively final and the lambda is rejected. If it were allowed, the lambda's copy and the real variable would disagree, and with threads, they'd race.

Any reassignment counts

Reassigning the variable anywhere disqualifies it, even before the lambda. The compiler looks at the whole method, not just the lines after the lambda.

A variable that changes once

✗ Not effectively final
String prefix = "Hi ";
if (formal) {
    prefix = "Dear ";
}
names.forEach(n -> IO.println(prefix + n));

One reassignment, even before the lambda, and the capture fails.

✓ Computed once
String prefix = formal ? "Dear " : "Hi ";
names.forEach(n -> IO.println(prefix + n));

Assigned exactly once, so it's effectively final.

The variable is frozen, not the object

The rule is about reassigning the variable. The object it points to can still change: list.add(...) inside a lambda is fine, and so is changing an array's contents. (An AtomicInteger or a stream's count() is usually cleaner.)

List<String> seen = new ArrayList<>();
names.forEach(n -> seen.add(n)); // ✓
// seen itself is never reassigned
🔮 Predict it

The array trick

What does this print?

int[] hits = {0};
var letters = List.of("a", "b", "c", "d");
letters.forEach(s -> hits[0]++);
System.out.println(hits[0]);
  1. 0
  2. 4
  3. Compile error
Show the answer

The variable hits (a reference to the array) never changes; only the array's contents do. That's allowed: 4.

🔮 Predict it

Fields are different

In a compact source file, top-level int total is an instance field. What prints?

int total = 0;
void main() {
    List.of(5, 5).forEach(n -> total += n);
    System.out.println(total);
}
  1. 0
  2. 10
  3. Compile error
Show the answer

The rule only covers local variables. Lambdas reach fields through this, so they can modify them: 5 + 5 = 10.

💼 In the real world

Why the rule protects you

Lambdas often run later or on other threads (streams, executors, event handlers). Banning reassigned locals rules out a whole class of data races. In practice, prefer returning results (stream().mapToInt(...).sum()) over mutating captured state.

Key takeaways

  1. Effectively final = never reassigned, even without the final keyword
  2. Reassigning anywhere (before or after the lambda) breaks it
  3. The variable is fixed, not the object: list.add() inside a lambda is fine
  4. Instance and static fields can be modified from lambdas

💡 A lambda takes a photo of your local variables — the photo can't update if the variable changes later.

🤯 Did you know?

Before Java 8, anonymous classes could only capture locals explicitly declared final. Java 8 relaxed that to "effectively final", which is why you rarely see the final keyword in front of captured variables today.

Practice questions

What does this print?

int count = 0;
List.of("a", "b").forEach(s -> count++);
System.out.println(count);
  1. 2
  2. 0
  3. Compile error
  4. 1
Check your answer

Compile error. count++ modifies a captured local variable, so count isn't effectively final and the lambda is rejected.

What does this print?

int[] count = {0};
List.of("a", "b", "c").forEach(s -> count[0]++);
System.out.println(count[0]);
  1. 0
  2. 3
  3. Compile error
  4. 1
Check your answer

3. The variable count (a reference to the array) never changes; only the array's contents do. That's allowed, though an AtomicInteger or a stream count() is usually cleaner.

Next: inside a lambda, what does this mean? The answer differs from an anonymous class in a way that surprises many developers.