Effectively Final Variables and Lambda Capture

Naveen AnamaneniUpdated · 5 min read
mediumJava 25javalambdainterview

A lambda can use variables from the method around it. This is called capturing a variable. Java allows a lambda to capture a local variable only if that variable is final or effectively final. A variable is effectively final if it is never changed after it is first given a value, even though it isn't marked with the final keyword. In this lesson, you will learn this rule, why it exists, and what to do when you need a value that changes.

What Is Effectively Final?

Example
int limit = 50;          // effectively final: never changed
int count = 0;
count++;                 // count is NOT effectively final

If you could add final to a variable's declaration without causing a compile error, it is effectively final.

Example: Capturing a Local Variable

Let's see an example where a lambda uses a local variable.

Capture.java▶ Try it
import java.util.List;
 
public class Capture {
    public static void main(String[] args) {
        int passMark = 50;
        List<Integer> marks = List.of(91, 38, 85, 42);
 
        marks.forEach(m -> {
            String result = m >= passMark ? "pass" : "fail";
            System.out.println(m + " " + result);
        });
    }
}
Output
91 pass
38 fail
85 pass
42 fail

Explanation: passMark is never changed after it is set, so it is effectively final, and the lambda can read it. The lambda captures a copy of its value.

What Happens If the Variable Changes

If the variable is changed anywhere in the method, before or after the lambda, the code doesn't compile:

Counter.java
import java.util.List;
 
public class Counter {
    public static void main(String[] args) {
        int total = 0;
        List.of(91, 78, 85).forEach(m -> total += m);
        System.out.println(total);
    }
}
Compiler output
Counter.java:6: error: local variables referenced from a lambda expression must be final or effectively final
        List.of(91, 78, 85).forEach(m -> total += m);
                                         ^
1 error

Explanation: total += m tries to change total inside the lambda, so total is no longer effectively final, and the compiler rejects it.

Why Java Has This Rule

  • A lambda may run later, after the method has returned and its local variables are gone. So the lambda keeps a copy of each captured value. If the variable could change, the copy and the original would disagree, which would be confusing.
  • A lambda may run on another thread. Allowing two threads to change the same local variable would cause hidden bugs. Fixing the value avoids them.

Workarounds for Changing Values

Workarounds.java▶ Try it
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
 
public class Workarounds {
    static int fieldTotal = 0;
 
    public static void main(String[] args) {
        List<Integer> marks = List.of(91, 78, 85);
 
        int streamTotal = marks.stream().mapToInt(Integer::intValue).sum();
        System.out.println("Stream sum: " + streamTotal);
 
        AtomicInteger atomic = new AtomicInteger();
        marks.forEach(m -> atomic.addAndGet(m));
        System.out.println("AtomicInteger: " + atomic.get());
 
        marks.forEach(m -> fieldTotal += m);
        System.out.println("Static field: " + fieldTotal);
 
        int loopTotal = 0;
        for (int m : marks) {
            loopTotal += m;
        }
        System.out.println("Plain loop: " + loopTotal);
    }
}
Output
Stream sum: 254
AtomicInteger: 254
Static field: 254
Plain loop: 254

Explanation: The best fix is usually to let a stream do the calculation (sum()), so nothing needs to change. An AtomicInteger works because the reference never changes, only the object's contents. A field isn't a local variable, so the rule doesn't apply. And a plain loop needs no lambda at all.

Common Mistakes

Using a one-element array (int[] total = {0}) as a habit. It works, but hides intent; prefer a stream or a loop.

Thinking the lambda sees later changes. It captured a fixed value.

Thinking the rule applies to fields. Lambdas can read and change fields freely, although that is often poor style.

Interview Corner

Why must variables used in a lambda be effectively final?

A lambda captures the values of local variables, not the variables themselves, because it may run after the method returns or on another thread. If a captured variable could change, the lambda's copy and the variable would disagree and could create race conditions. Requiring the variable to be final or effectively final keeps the copy and the original consistent. The rule applies to local variables and parameters, not to fields.

Practice

  1. Easy: Write a method that receives a String prefix and prints every name in a list with that prefix, using forEach and a lambda.
  2. Harder: Count the names longer than five letters in a list in three ways: a stream with count(), an AtomicInteger with forEach, and a for-each loop.

Quick check: Lambda capture

4 questions
1When is a local variable effectively final?
2Can a lambda change a captured local variable?
3Can a lambda change an instance field?
4What is usually the best fix when you want a running total?

Key Takeaways

Was this tutorial helpful?

Lesson 305 of 482