Definition
- Value capture: a lambda copies the local variable's VALUE into its closure. It does not share the stack slot.
- Effectively final: the variable is assigned once and never reassigned. Java enforces this at compile time.
- Why: if the variable changed after capture, the lambda's copy and the original would disagree, a correctness bug.
- AtomicInteger workaround: the reference is effectively final, but the internal value is mutable via thread-safe CAS operations.
- int[] workaround: the array reference is final, but the contents are mutable. NOT thread-safe for parallel streams.
- Best practice: replace mutable capture with
reduceorcollect, which return the result instead of mutating external state.
At National Bank of India, a clerk makes a photocopy of a customer's balance slip before processing a transaction. If the original balance changes while the clerk is processing (another teller updates it), the photocopy is stale. Java prevents this by requiring the balance slip to be "effectively final": once photocopied, the original cannot change.
Understanding the Concepts
Why are captured variables required to be effectively final?
A lambda may outlive the method's stack frame. It can be stored in a field, passed to another thread, or executed later. Java copies the local variable's VALUE into the lambda rather than sharing the stack slot. If the variable could change after capture, the copy and the original would disagree.
// DOES NOT COMPILE: count is not effectively final
int count = 0;
accounts.forEach(a -> count++); // Compile error!
What is effectively final?
A local variable is effectively final if it is assigned exactly once and never reassigned, even without the final keyword.
int limit = 100; // effectively final: assigned once, never changed
accounts.stream()
.filter(a -> a.balance().compareTo(BigDecimal.valueOf(limit)) > 0)
.toList(); // OK: limit is effectively final
int threshold = 100;
threshold = 200; // reassigned!
accounts.stream()
.filter(a -> a.balance().compareTo(BigDecimal.valueOf(threshold)) > 0)
.toList(); // COMPILE ERROR: threshold is not effectively final
What are the workarounds?
package com.bank.analytics;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class MutableCaptureDemo {
record Account(String id, BigDecimal balance) {}
public static void main(String[] args) {
List<Account> accounts = List.of(
new Account("SAV2001", new BigDecimal("50000")),
new Account("SAV2002", new BigDecimal("150000")),
new Account("SAV2003", new BigDecimal("75000"))
);
// Workaround 1: AtomicInteger (thread-safe)
AtomicInteger count = new AtomicInteger(0);
accounts.forEach(a -> count.incrementAndGet());
System.out.println("Count (Atomic): " + count.get());
// Workaround 2: final int[] (NOT thread-safe)
int[] countArr = {0};
accounts.forEach(a -> countArr[0]++);
System.out.println("Count (array): " + countArr[0]);
}
}
What is the best approach?
Replace mutable capture with reduce or collect.
// WRONG: mutable capture (side effect)
AtomicReference<BigDecimal> total = new AtomicReference<>(BigDecimal.ZERO);
accounts.forEach(a -> total.updateAndGet(t -> t.add(a.balance())));
// CORRECT: pure reduce (no mutation)
BigDecimal total = accounts.stream()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
The reduce approach is pure (no side effects), parallelizable, and idiomatic.
Why do instance fields not have this restriction?
public class Counter {
private int count = 0; // instance field, not local variable
public void process(List<Account> accounts) {
accounts.forEach(a -> count++); // OK: fields are on the heap
}
}
Instance fields live on the heap, not the stack. The lambda accesses them through the this reference, which is effectively final. The field itself can change because the lambda reads from the heap each time, not from a copy.
What are common mistakes?
WRONG: Using the int[] trick in parallel streams.
// WRONG: race condition in parallel stream
int[] count = {0};
accounts.parallelStream().forEach(a -> count[0]++); // RACE CONDITION!
// count[0] may be less than accounts.size()
CORRECT: Use AtomicInteger for parallel, or better, use count().
// CORRECT: stream count()
long count = accounts.parallelStream().count();
What should a senior engineer know?
- The array/AtomicInteger trick signals that you are fighting the functional model. Prefer
reduce/collectwhich return the result instead of mutating captured state. - In Kotlin, lambdas CAN capture and modify local variables because Kotlin wraps them in a
Refobject on the heap. Java chose the stricter approach for safety. - The effectively-final rule also applies to local variables captured by anonymous inner classes, not just lambdas.
Visual Overview
Local variable capture:
Method stack frame
┌───────────────────┐
│ int count = 0 │
└────────┬──────────┘
│ COPY value
▼
Lambda closure
┌───────────────────┐
│ captured: 0 │ ← This is a snapshot
│ │ If count changed,
│ │ closure is stale!
└───────────────────┘
Java prevents this by requiring count to be
effectively final (never reassigned after init).
Key Takeaway
Lambdas capture VALUE copies of local variables. If the variable changed after capture, the lambda would hold a stale copy, causing correctness bugs. Java prevents this by requiring captured local variables to be effectively final (assigned once, never reassigned).
The recommended workaround is AtomicInteger whose reference is effectively final but whose internal value is mutable via thread-safe CAS. The int[] workaround works for sequential streams but causes race conditions in parallel. The best approach is to replace mutable capture with reduce or collect, which return the result instead of mutating external state.
At National Bank of India, a running total inside a forEach cannot be a plain int total (reassignment is illegal). Using reduce(ZERO, BigDecimal::add) produces the total as a pure result without side effects.
Remember one sentence: lambdas capture value copies, so Java requires effectively-final locals, and the clean fix is reduce/collect instead of mutable capture.
---