Back to Stream: filter
Q3Question 3 of 19

Effectively final variable inside filter(): capture rules, AtomicInteger workaround, final array

Key Concepts
  • Lambdas inside filter capture a COPY of local variables, so those variables must be effectively final (never reassigned).
  • Attempting to modify a captured variable (count++) inside a lambda produces a compile error.
  • AtomicInteger is the recommended workaround: the reference is final but the internal value is mutable via thread-safe CAS operations.
  • final int[] is a workaround for sequential streams but is NOT thread-safe for parallel streams due to non-atomic read-modify-write.
  • Instance fields are not local variables and can be used inside lambdas without the effectively final restriction.
  • Effectively final means the reference is never reassigned; the object's internal state can still change (AtomicInteger, List, Map).
  • In parallel streams, always use AtomicInteger or AtomicLong instead of int[] to avoid race conditions.

Definition

Lambdas capture a copy of local variables which must be effectively final; use AtomicInteger for thread-safe mutable counters or final int[] for sequential-only workarounds.
  • Effectively final: a local variable that is either declared final or 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?

JAVA
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?

JAVA
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);
    }
}
TEXT
Checked: 4 amounts
Large: [2000, 3000]

Why is int[] unsafe in parallel streams?

JAVA
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
    }
}
TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate