Effectively Final Variables and Lambda Capture
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?
int limit = 50; // effectively final: never changed
int count = 0;
count++; // count is NOT effectively finalIf 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.
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);
});
}
}91 pass
38 fail
85 pass
42 failExplanation: 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:
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);
}
}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 errorExplanation: 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
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);
}
}Stream sum: 254
AtomicInteger: 254
Static field: 254
Plain loop: 254Explanation: 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
- Easy: Write a method that receives a
String prefixand prints every name in a list with that prefix, usingforEachand a lambda. - Harder: Count the names longer than five letters in a list in three ways: a stream with
count(), anAtomicIntegerwithforEach, and a for-each loop.
Key Takeaways
Was this tutorial helpful?