skip to content

A lambda needs to accumulate a running total across iterations, but the compiler rejects reassigning a captured local. What are the correct ways to achieve mutable state, and what are the trade-offs?

level: seniorimportance: should knowfreq 50%

answer

  1. Can't reassign the captured local; CAN mutate the object it points to
  2. array[0] box / AtomicLong / field are the three escapes
  3. array box = terse but NOT thread-safe
  4. Atomic* = thread-safe, clearer intent
  5. Parallel stream -> use reduction, not mutable capture

basics

~20 s

You can't reassign a captured local. Instead, mutate something the lambda only reads a reference to: a one-element array, an AtomicInteger, or a field on an object. For thread-safe accumulation, prefer AtomicInteger/AtomicLong or a proper reduction.

solid answer

~40 s

The effectively-final rule blocks reassigning a captured local, but it does not stop you from mutating an object whose *reference* is effectively final. Common patterns: (1) a one-element array `long[] sum = {0}` and mutate `sum[0]`; (2) a mutable holder such as `AtomicLong` / `AtomicInteger`, mutated via `getAndAdd`; (3) an instance field, since fields are accessed live through `this`. Trade-offs: the array trick is terse but offers no thread safety and signals a code smell; `Atomic*` is thread-safe and clearer; a field couples the state to the object. Crucially, if the lambda runs in a parallel stream, naive shared mutation is a data race — the right answer there is usually a **reduction** (`mapToLong(...).sum()`, `reduce`, or a `Collector`), which avoids shared mutable state entirely. Reach for mutable capture only for genuinely sequential side effects.

code

java · 14 lines
java
// Does NOT compile: reassigns a captured local
// int total = 0;
// nums.forEach(n -> total += n);

// 1) mutable box (sequential only, not thread-safe)
long[] box = {0};
nums.forEach(n -> box[0] += n);

// 2) atomic holder (thread-safe)
AtomicLong sum = new AtomicLong();
nums.forEach(n -> sum.getAndAdd(n));

// 3) preferred for streams: a reduction, no shared mutable state
long total = nums.stream().mapToLong(Integer::longValue).sum();

go deeper

for a junior

Knows the compiler rejects reassigning a captured local and that one fix is a one-element array or a field.

for a middle

Can list array box, Atomic*, and field, and explains that it's the reference (not the object) that must stay final.

for a senior

Weighs the trade-offs (thread safety, clarity, ownership) and knows that stream aggregation should use a reduction/collector rather than mutable capture.

for a principal

Reasons about contention and parallel-stream performance, steers teams toward stateless reductions and immutability, and recognizes mutable capture as a smell to be justified.

## The constraint, restated A lambda **captures local variables by value** and only permits **effectively final** locals (assigned once, never reassigned). So `int total = 0; list.forEach(x -> total += x);` does **not compile** — `total += x` reassigns `total`. The key insight: the rule forbids reassigning the *captured variable*, not mutating the *object it points to*. If the captured thing is a reference to a mutable object and that reference never changes, you may change the object's contents all you like. ## The three idiomatic escapes ### 1. One-element array (the 'mutable box' trick) ```java long[] sum = {0}; // the array reference is effectively final list.forEach(x -> sum[0] += x); // mutating sum[0] is fine long total = sum[0]; ``` Works because `sum` (the reference) is never reassigned; only element 0 changes. Terse but cryptic, and **not thread-safe**. ### 2. Atomic holder ```java AtomicLong sum = new AtomicLong(); list.forEach(x -> sum.getAndAdd(x)); long total = sum.get(); ``` `AtomicLong`/`AtomicInteger`/`AtomicReference` are designed mutable holders with **lock-free, thread-safe** updates. Clearer intent than the array and safe under concurrency. Slight overhead vs a plain field. ### 3. Instance field ```java class Accumulator { long sum; void add(int x) { /* lambda body */ this.sum += x; } } ``` Fields are read/written live through the captured `this`, so they bypass the local-capture rule entirely. Good when the state genuinely belongs to the object; otherwise it leaks lambda-internal state onto the class. ## Why none of these are the *best* answer for streams With a **parallel stream**, multiple threads run the lambda simultaneously. The array trick races outright (lost updates, torn reads). Even `AtomicLong` works but serializes on a hot contended counter, killing the parallelism you paid for. The functional-programming-correct solution is a **reduction**: combine values without shared mutable state. ```java long total = list.stream().mapToLong(Integer::longValue).sum(); // built-in reduction long total2 = list.stream().reduce(0, Integer::sum); // explicit reduce ``` Reductions are **associative** and stateless per element, so the framework can split, process partitions independently, and merge — correct and fast in parallel. This is why the Streams API exposes `sum`, `reduce`, and `Collector`s instead of encouraging side-effecting `forEach`. ## Decision guide - Sequential, quick local accumulation, single-threaded, throwaway code → array box or `Atomic*` (prefer `Atomic*` for readability). - Concurrent updates from multiple threads → `Atomic*` or a concurrent collection. - Stream aggregation → a **reduction / collector**, not mutable capture. - State conceptually owned by an object → a field. ## Underlying principle The language nudges you away from shared mutable state because that state is the source of data races and hard-to-reason-about code. The 'effectively final' rule is a small wall that makes you pause and pick an explicit, intention-revealing mechanism instead of silently closing over a mutable local.

  • Why is the one-element array trick dangerous in a parallel stream?
    Multiple threads read-modify-write `array[0]` without synchronization, producing a data race: updates are lost and reads can be torn. Use a reduction (or at least an Atomic) instead; better yet, avoid shared mutable state entirely.
  • Is `Stream.reduce` always preferable to a mutable accumulator?
    For aggregating values into a single result, yes — it's stateless per element, associative, and parallel-safe. But for accumulating into a mutable container, `collect` with a `Collector` (mutable reduction) is the designed tool; raw side-effecting `forEach` is the thing to avoid.

The effectively-final rule locks the mailbox's address (the reference), not its contents. You can keep stuffing letters into the same mailbox (mutate the object); you just can't swap the box for a new one (reassign the variable).

saying these in an interview costs you the question

  • Claiming the array/AtomicInteger trick 'violates' the effectively-final rule — it doesn't; the reference stays final, only the contents change.
  • Recommending the array box for concurrent/parallel code — it's not thread-safe.
  • Using side-effecting forEach with shared mutation as the default for stream aggregation instead of a reduction/collector.
  • Assuming AtomicLong fully solves parallel performance — a single contended counter serializes threads and can negate parallelism.

context