Capturing Variables in Lambda Expressions
Lambda expressions can access variables declared in the surrounding code. This is known as variable capturing.
There are, however, some important rules — and one important best practice — to understand.
Capturing a Local Variable
Suppose we want to print products whose price is below a given limit:
double priceLimit = 5.00;
printProducts(
products,
p -> p.getPrice() < priceLimit
);Here, priceLimit is declared outside the lambda but used inside it.
We say that the lambda captures the variable priceLimit.
Conceptually:
Surrounding Code
↓
priceLimit = 5.00
↓
Lambda captures it
↓
p -> p.getPrice() < priceLimitThe Effectively Final Rule
A local variable captured by a lambda must be either:
- explicitly
final, or - effectively final
A variable is effectively final when it is assigned once and never reassigned.
This works:
double priceLimit = 5.00;
products.forEach(
p -> System.out.println(p.getPrice() < priceLimit)
);Even though we didn’t write:
final double priceLimit = 5.00;Java treats priceLimit as effectively final because its value never changes.
Reassignment Is Not Allowed
This will not compile:
double priceLimit = 5.00;
priceLimit = 6.00;
products.forEach(
p -> System.out.println(p.getPrice() < priceLimit)
);The same applies if the reassignment happens after the lambda:
double priceLimit = 5.00;
products.forEach(
p -> System.out.println(p.getPrice() < priceLimit)
);
priceLimit = 6.00; // still a problemJava considers the variable across its entire scope when determining whether it is effectively final.
Why Does Java Have This Rule?
A lambda can potentially outlive the method in which it was created.
For example:
double priceLimit = 5.00;
Runnable task = () ->
System.out.println(priceLimit);The lambda might execute on another thread after the surrounding method has already returned.
Local variables normally live in the method’s stack frame and disappear when the method finishes.
Therefore, captured local variables are effectively captured by value. The lambda works with the captured value rather than depending on the original stack variable continuing to exist.
Requiring the variable to remain effectively final also prevents ambiguity about which value the lambda should observe.
You Cannot Modify a Captured Local Variable
The rule works both ways.
You cannot modify the captured variable from inside the lambda either.
For example:
int count = 0;
products.forEach(p -> {
if (p.getPrice() < 5.00) {
count++; // Compile error
}
});count is captured by the lambda.
But count++ modifies it, meaning it is no longer effectively final.
Get Suraj’s stories in your inbox
Join Medium for free to get updates from this writer.
Therefore, the code doesn’t compile.
What About Mutable Objects?
There is an important distinction between:
changing a variable
and
changing the object referenced by that variable.
For example:
List<Product> cheapProducts = new ArrayList<>();
products.forEach(p -> {
if (p.getPrice() < 5.00) {
cheapProducts.add(p);
}
});This compiles.
Why?
Because we are not reassigning cheapProducts.
We are modifying the object it references.
This would not compile:
cheapProducts = new ArrayList<>(); //reassigningBut this is allowed:
cheapProducts.add(product); //modifying the object it referencesSo final or effectively final applies to the variable reference, not necessarily to the state of the referenced object.
Be Careful with Side Effects
Just because modifying an external object from a lambda is allowed doesn’t mean it is a good idea.
Consider:
List<Product> cheapProducts = new ArrayList<>();
products.forEach(p -> {
if (p.getPrice() < 5.00) {
cheapProducts.add(p);
}
});The lambda has a side effect because it modifies state outside itself.
This becomes especially dangerous when concurrency enters the picture.
For example, changing the processing to a parallel stream:
products.parallelStream()
.forEach(p -> {
if (p.getPrice() < 5.00) {
cheapProducts.add(p);
}
});may cause multiple threads to modify the same mutable collection concurrently.
That can introduce race conditions, incorrect results, or other concurrency problems.
A better functional style is to describe the transformation itself:
List<Product> cheapProducts =
products.stream()
.filter(p -> p.getPrice() < 5.00)
.toList();This avoids manually mutating external state.
Key Takeaways
- Lambdas can capture variables from their surrounding scope.
- Captured local variables must be final or effectively final.
- Reassigning a captured variable — before or after the lambda — breaks the effectively final rule.
- A lambda cannot modify a captured local variable.
- A lambda can modify the state of an object referenced by a captured variable.
- However, modifying external state introduces side effects and should generally be avoided.
- Side effects become particularly problematic with parallel execution.
- Prefer functional operations such as
filter(),map(),reduce(), and collection operations over mutating external state.
In short: Lambdas can read captured local values, but those variables must remain stable. For cleaner and safer functional code, think “capture values, avoid shared mutable state.”
Continue Learning Java Lambdas 🚀
← Previous: Syntax of Lambda Expressions | Next →: Interaction of Lambda Expressions with Enclosing Code










