Definition
- Effectively final: a local variable that is either declared
finalor never reassigned after initialisation. - Copy semantics: the lambda captures the value (a copy), not the variable binding itself.
- Compile error:
int count = 0; filter(t -> count++)fails because count is reassigned. - AtomicInteger: the reference is effectively final but the internal value is mutable via
incrementAndGet()(thread-safe). - final int[]: the array reference is final but contents are mutable via
count[0]++(NOT thread-safe). - Instance fields: not local variables, so they have no effectively final restriction, but they are not thread-safe either.
When a clerk (lambda) at National Bank of India is assigned to a desk, they are given a photocopy of the rulebook (captured variable). If the original rulebook changes later, the clerk's photocopy is stale. So the bank (compiler) says: the rulebook must be final. If you need a changing value like a counter, put it in a lockbox (AtomicInteger): the lockbox reference never changes (final), but you can update the number inside the lockbox.
Understanding the Concepts
What is the effectively final rule?
Local variables used inside a lambda must be effectively final: either declared final or never reassigned after initialisation. This is because the lambda captures a copy of the value, not a reference to the variable binding.
What does NOT compile?
package com.bank.core;
import java.math.BigDecimal;
import java.util.List;
public class EffectivelyFinalError {
public static void main(String[] args) {
List<BigDecimal> amounts = List.of(
new BigDecimal("1000"), new BigDecimal("2000"), new BigDecimal("3000")
);
// WRONG: count is reassigned inside lambda
// int count = 0;
// amounts.stream().filter(a -> {
// count++; // COMPILE ERROR! Not effectively final
// return a.compareTo(new BigDecimal("1500")) > 0;
// });
// WRONG: threshold is reassigned before lambda use
// int threshold = 1000;
// if (true) { threshold = 2000; } // reassigned!
// amounts.stream().filter(a -> a.intValue() > threshold); // ERROR
}
}
What is the AtomicInteger workaround?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterDemo {
public static void main(String[] args) {
List<BigDecimal> amounts = List.of(
new BigDecimal("1000"), new BigDecimal("2000"),
new BigDecimal("3000"), new BigDecimal("500")
);
AtomicInteger checkedCount = new AtomicInteger(0); // reference is final
BigDecimal threshold = new BigDecimal("1500");
List<BigDecimal> large = amounts.stream()
.filter(a -> {
checkedCount.incrementAndGet(); // OK: reference is final, value is mutable
return a.compareTo(threshold) > 0;
})
.toList();
System.out.println("Checked: " + checkedCount.get() + " amounts");
System.out.println("Large: " + large);
}
}
Checked: 4 amounts
Large: [2000, 3000]
Why is int[] unsafe in parallel streams?
package com.bank.core;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class ParallelSafetyDemo {
public static void main(String[] args) {
// UNSAFE: int[] in parallel
final int[] unsafeCount = {0};
IntStream.range(0, 100_000).parallel()
.forEach(n -> unsafeCount[0]++); // race condition!
System.out.println("Unsafe count: " + unsafeCount[0]); // often less than 100000
// SAFE: AtomicInteger in parallel
AtomicInteger safeCount = new AtomicInteger(0);
IntStream.range(0, 100_000).parallel()
.forEach(n -> safeCount.incrementAndGet()); // CAS: thread-safe
System.out.println("Safe count: " + safeCount.get()); // always 100000
}
}
Unsafe count: 97834
Safe count: 100000
What about instance fields?
Instance and static fields are not local variables and have no effectively final restriction. However, they share the same thread-safety concerns in parallel streams.
What are the rules?
| Approach | Sequential safe? | Parallel safe? | Recommended? |
|----------|-----------------|----------------|--------------|
| int variable | Compile error | Compile error | No |
| AtomicInteger | Yes | Yes | Yes |
| final int[] | Yes | No (race) | Sequential only |
| Instance field | Yes | No (race) | Use with caution |
What should a senior engineer know?
Effectively final is about the reference, not the value. AtomicInteger counter = new AtomicInteger(0) is effectively final because the reference counter is never reassigned, but the value inside changes freely. The same principle applies to any final reference to a mutable object: final List, final Map, final StringBuilder. In parallel streams, always prefer atomic types over mutable containers.
Visual Overview
Effectively Final Rule:
───────────────────────
int count = 0;
lambda uses count
count++; ← reassignment! COMPILE ERROR
Workaround:
┌──────────────────────────────┐
│ AtomicInteger count = new(0) │ ← reference is final
│ count.incrementAndGet() │ ← internal value changes
│ Thread-safe in parallel │
└──────────────────────────────┘
final int[] count = {0};
count[0]++; ← array reference is final
contents mutable
NOT thread-safe!
Key Takeaway
Local variables used inside lambdas must be effectively final because the lambda captures a copy of the value, not a reference to the variable binding. The compiler enforces this to prevent stale-data bugs.
AtomicInteger is the recommended workaround: the reference is effectively final but the internal value is mutable via thread-safe CAS operations. The final int[] approach works for sequential streams but causes race conditions in parallel streams because count[0]++ is a non-atomic read-modify-write operation.
The key insight is that effectively final is about the reference, not the value. Any final reference to a mutable object (AtomicInteger, List, Map) satisfies the compiler while allowing internal state changes.
Remember one sentence: lambdas capture copies of local variables which must be effectively final; use AtomicInteger for thread-safe mutable counters.
---