skip to content

What does 'effectively final' mean, and why must variables captured by a lambda be final or effectively final?

level: seniorimportance: should knowfreq 60%

answer

  1. effectively final = never reassigned, Java 8
  2. lambdas/anon classes capture locals by value
  3. stack frame may vanish / run on another thread
  4. copy vs original would diverge if reassignable
  5. workaround: array[0] or AtomicInteger (heap mutation)

basics

~20 s

A local variable is effectively final if you never reassign it after its first value, even though you did not write the word final. Lambdas and anonymous classes can only use such variables, because they capture the value, not a live link to your method's variable.

solid answer

~50 s

Effectively final, introduced in Java 8, means a local variable that is never reassigned after initialization, so it could have been declared final without changing the code. Lambdas and anonymous inner classes may only capture local variables that are final or effectively final. The reason is capture-by-value: when a lambda captures a local, it copies the value into the lambda's own state, because the local lives on the method's stack frame which may disappear before the lambda runs (and the lambda may run on another thread). If the variable could be reassigned afterwards, the lambda's copy and the local would diverge, creating confusing, racy semantics. So Java requires the variable to be unchangeable, guaranteeing the captured copy always matches the source. A common workaround for needing mutation is to capture a mutable holder (array element or AtomicInteger), but that is usually a smell.

code

java · 10 lines
java
void demo(List<String> items) {
    int prefixLen = 3;                  // effectively final
    items.stream()
         .filter(s -> s.length() >= prefixLen) // captures prefixLen by value
         .forEach(System.out::println);
    // prefixLen = 4; // adding this breaks the capture above

    AtomicInteger seen = new AtomicInteger();
    items.forEach(s -> seen.incrementAndGet()); // mutate heap object, not reassign local
}

go deeper

for a junior

Knows a lambda can use a local variable only if it is not reassigned.

for a middle

Defines effectively final and recognizes the compile error when a captured local is later reassigned.

for a senior

Explains capture-by-value, the stack-frame/threading rationale, and the fields-vs-locals distinction with the holder workaround.

for a principal

Reasons about closure semantics across languages, escape/threading implications, and steers teams toward immutable, side-effect-free lambdas over mutable-holder hacks.

## The term 'effectively final' Introduced in **Java 8**, a local variable (or method parameter) is **effectively final** if it is **assigned once and never reassigned afterward**, even though you did not actually write the `final` keyword. The test: *would the code still compile if you added `final` to the declaration?* If yes, it is effectively final. ```java int a = 5; // effectively final (never reassigned) int b = 5; b = 7; // NOT effectively final (reassigned) ``` ## The capture rule A **lambda** (`x -> x + 1`) and an **anonymous inner class** (`new Runnable() {...}`) can use local variables from the enclosing method — this is called **capturing**. Java requires any captured local variable to be **final or effectively final**: ```java int base = 10; // effectively final -> OK Runnable r = () -> System.out.println(base); // base = 20; // would break: base is now reassigned -> compile error ``` If you add `base = 20;` anywhere, the capture above stops compiling. ## Why the restriction exists: capture by value Local variables live on the **stack frame** of the method that declares them. That frame is destroyed when the method returns. But a lambda or anonymous instance can **outlive** the method — it can be stored, returned, or run later on a different thread. So the lambda cannot keep a live pointer to the stack slot; instead it **copies the value** into its own object state at capture time. This is **capture by value**. Now imagine reassignment were allowed: ```java int counter = 0; Runnable r = () -> System.out.println(counter); // captured copy = 0 counter = 5; // (hypothetically allowed) r.run(); // would it print 0 or 5? ``` The lambda holds a copy (`0`); the local is now `5`. The two have diverged, and there is no sane, race-free way to keep them in sync — especially across threads. Rather than expose this confusing behavior (as some languages do with shared mutable closures), Java sidesteps it by **requiring the variable not to change**. Then the copy and the original can never differ, and the value is well-defined. Contrast with **instance/static fields**, which a lambda accesses through the captured `this` (or the class) — those live on the heap, are not copied, and so they may be mutated and even seen changing through the lambda. The final/effectively-final rule applies **only to captured locals**, not to fields. ## The mutable-holder workaround Sometimes you genuinely want a lambda to mutate a counter. You cannot reassign a captured local, but you can mutate the **object** a (final/effectively-final) local points to: ```java int[] count = {0}; // the reference is effectively final list.forEach(x -> count[0]++); // mutating the array element is fine // or AtomicInteger for thread-safety AtomicInteger n = new AtomicInteger(); list.forEach(x -> n.incrementAndGet()); ``` This works because you are mutating the heap object, not reassigning the local. It is a recognized workaround but often a sign you should restructure (e.g. use a stream `reduce`/`count` instead). ## Summary - Effectively final = never reassigned, so `final` could be added with no change. - Captured locals must be final or effectively final. - Reason: locals are captured **by value** (copied) because the stack frame may be gone; forbidding reassignment keeps the copy and original from diverging. - Use a mutable holder or atomic if you truly must mutate from a lambda.

  • Why can a lambda freely mutate an instance field but not reassign a captured local?
    An instance field lives on the heap and is reached through the captured this reference, so the lambda sees the live field. A local lives on the stack frame that may be gone, so it is captured by value (copied); allowing reassignment would let the copy and original diverge.
  • How do you increment a counter inside a forEach lambda?
    Capture a mutable holder instead of reassigning a local: use an AtomicInteger (thread-safe) or a single-element int[] array and mutate its contents. Better still, use a stream operation like count() or reduce().

Capturing a local is like photocopying a number off a whiteboard before the room is demolished. If someone could later edit the whiteboard, your photocopy would be stale — so Java forbids editing the original after the copy is taken.

saying these in an interview costs you the question

  • Saying captured locals are captured by reference in Java.
  • Claiming you must always write final explicitly to use a variable in a lambda.
  • Thinking the rule also applies to instance/static fields.
  • Believing the int[] workaround is reassigning the local (it mutates the object).

context