Effectively final and variable capture
Effectively final locals, lambda variable capture, fields and `this`, holder workarounds, capturing allocation costs and loop variable edge cases.
On this page
🎒 What is effectively final variable capture?
A local variable is effectively final when you assign it once and never reassign it, even if you did not write the final keyword. Java lets lambdas, anonymous classes and local classes capture only final or effectively final locals. For example, int limit = 10; orders.stream().filter(o -> o.cents() > limit) compiles, but limit++ anywhere after that makes the lambda line fail.
For this lesson, we'll focus on local variable capture in lambdas, anonymous classes and local classes, why the rule exists, how fields and this are different, the holder workaround smell, capturing versus non-capturing lambda allocation, and loop variable capture. Interviewers ask this because the compiler error is common, but the real answer is about stack frames, copied values and mutability boundaries.
Below the line (out of scope)
- Functional interfaces and
invokedynamicbasics: Lambdas and functional interfaces - Method-reference receiver timing: Method references
- The broader
finalkeyword and final fields: The final keyword in Java - Parallel stream safety and data races: covered in the Concurrency module
The 30-second answer
Effectively final means a local variable or parameter is never reassigned after its initial value. Lambdas can capture such locals because Java copies the value into the lambda object, or passes it as an extra argument to the generated lambda body. There is no live shared stack slot between the method and the lambda. If reassignment were allowed, the copied value and the local variable could disagree, especially after the method returns or when the lambda runs on another thread. Fields are different: a lambda that reads this.count captures this, not a copy of count, so the field can change. Holder tricks like AtomicInteger or int[] compile because the holder reference is effectively final, but they usually signal that a reduction or a loop would be clearer.
What you’ll learn
- Define effectively final using the JLS rule for locals and parameters
- Explain why lambdas copy captured local values instead of sharing a stack slot
- Separate local capture from capturing **
this** and mutable instance fields - Spot
AtomicIntegerandint[1]holder workarounds as a code smell - Compare capturing and non-capturing lambdas, including HotSpot's cached singleton behavior
- Handle classic
forand enhancedforloop capture without interview surprises
Interview radar
Asked very often: Why does Java say "local variables referenced from a lambda expression must be final or effectively final"?
Core follow-up: What is captured: the variable, the value, or this?
Production angle: A local counter smuggled through AtomicInteger works in a sequential stream, then becomes misleading or unsafe when the pipeline changes.
01Picture it first: a boarding pass, not a walkie-talkie
Think of a captured local as a boarding pass copied into your bag. The gate agent writes your seat on the pass, you walk away, and the pass does not stay connected to the airline desk. If the desk later changes its screen, your paper copy does not magically update.
A mutable field is different. Capturing this is like carrying a key card to the airline office. Every time you use it, you walk back to the office and read the current screen.
A lambda copies an effectively final local value into its own object while a lambda that reads an instance field captures this and reads the field through the object each time
Notice the important line in the picture: locals are copied by value of the variable, while fields are reached through an object reference. If the local is a reference, the reference value is copied, not the whole object. The object can still be mutable.
02Variable capture under the hood
Try it yourself first
Before reading on, decide which line makes minCents stop being effectively final. Then say what the lambda should print.
import java.util.List;
import java.util.function.Predicate;
record Order(String id, int cents) { }
public class CaptureDemo {
public static void main(String[] args) {
int minCents = 1_000;
Predicate<Order> expensive = order -> order.cents() >= minCents;
List<Order> orders = List.of(new Order("o-1", 900), new Order("o-2", 1_400));
System.out.println(orders.stream().filter(expensive).count()); // 1
// minCents = 2_000; // uncommenting this makes the lambda line fail
}
}
The commented reassignment is enough to break effective finality. It does not matter that the reassignment appears after the lambda. The compiler analyzes the whole local variable and refuses the capture if the variable is assigned again.
Notice three things:
- The word final is optional.
final int minCents = 1_000;andint minCents = 1_000;behave the same here if there is no reassignment. - The rule covers locals and parameters. A method parameter used in a lambda must also be final or effectively final.
- Mutating the object is not reassignment. If
minCentswere aList<Integer> thresholds, callingthresholds.add(1_000)would not change whether the local reference is effectively final.
The JLS wording lives in two places. JLS 4.12.4 defines effectively final variables. JLS 15.27.2 says a local variable, formal parameter or exception parameter used but not declared in a lambda body must be final or effectively final.
Under the covers, the lambda does not keep a pointer to your method's local variable slot. A method's local variables live in that method's stack frame. Once the method returns, that frame is gone. A lambda can escape the method and run later, so Java captures the value it needs.
import java.util.function.Supplier;
public class EscapeDemo {
static Supplier<String> receiptPrinter(String orderId) {
String prefix = "paid:";
return () -> prefix + orderId;
}
public static void main(String[] args) {
Supplier<String> printer = receiptPrinter("o-42");
System.out.println(printer.get()); // paid:o-42
}
}
By the time printer.get() runs, receiptPrinter has already returned. The local variables prefix and orderId are gone from the stack, but their values were copied into the lambda instance. That is why the program still works.
If Java allowed prefix = "refunded:" after the lambda was created, we would need to answer an awkward question: should printer.get() read the old copy or the new local? Java avoids that split-brain model by allowing only locals that cannot be reassigned.
03Fields and this are captured differently
Try it yourself first
Before reading the explanation, predict the two numbers this prints. Is discountPct copied when the lambda is built?
import java.util.function.LongUnaryOperator;
public class PricingRules {
private int discountPct = 10;
LongUnaryOperator currentDiscount() {
return cents -> cents * (100 - discountPct) / 100;
}
public static void main(String[] args) {
PricingRules rules = new PricingRules();
LongUnaryOperator price = rules.currentDiscount();
System.out.println(price.applyAsLong(1_000)); // 900
rules.discountPct = 25;
System.out.println(price.applyAsLong(1_000)); // 750
}
}
The lambda reads the updated field. It captured this, and every call follows that reference to the current discountPct. There is no effectively-final rule for fields because fields do not live in a disappearing stack frame.
This is also where lambdas differ from anonymous classes in a way candidates often miss. Inside a lambda, this means the enclosing object. Inside an anonymous class, this means the anonymous object itself.
public class ThisDemo {
private final String name = "screen";
Runnable lambda() {
return () -> System.out.println(this.name); // this is ThisDemo
}
Runnable anonymous() {
return new Runnable() {
private final String name = "callback";
@Override public void run() {
System.out.println(this.name); // this is the anonymous Runnable
}
};
}
}
Use that fact in interviews. If the lambda body reads an instance field or calls an instance method, it keeps the enclosing object reachable. In UI code, listener registries and long-lived executors, that can turn into a memory leak.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Advanced: holder tricks, allocation and loop variables
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal