Effectively final capture in Java
Lambdas can only capture locals that are never reassigned.
int 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)); // ✓Your turn
What happens?
int sum = 0;
List.of(1, 2, 3).forEach(n -> sum += n);
System.out.println(sum);Prints 6Prints 0Compile 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
String prefix = "Hi ";
if (formal) {
prefix = "Dear ";
}
names.forEach(n -> IO.println(prefix + n));One reassignment, even before the lambda, and the capture fails.
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 reassignedThe 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]);04Compile error
Show the answer
The variable hits (a reference to the array) never changes; only the array's contents do. That's allowed: 4.
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);
}010Compile error
Show the answer
The rule only covers local variables. Lambdas reach fields through this, so they can modify them: 5 + 5 = 10.
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
- Effectively final = never reassigned, even without the final keyword
- Reassigning anywhere (before or after the lambda) breaks it
- The variable is fixed, not the object: list.add() inside a lambda is fine
- 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.
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);- 2
- 0
- Compile error
- 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]);- 0
- 3
- Compile error
- 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.