Variable Capture in Java Lambda Expressions
last modified May 25, 2025
This tutorial explores variable capture in Java lambda expressions, a fundamental concept in functional programming with Java. We'll cover the rules and practical examples of how lambdas interact with surrounding variables.
Variable capture in lambda expressions refers to the ability of a lambda to use variables declared outside its body. These variables are said to be captured by the lambda. Java has specific rules about which variables can be captured and how they can be used.
The most important rule is that any local variable, formal parameter, or exception parameter used but not declared in a lambda must be final or effectively final. This ensures predictable behavior when lambdas are executed, especially in concurrent contexts.
An effectively final variable is one whose value is never changed after it is initialized. While not explicitly declared with the final keyword, it can be treated as final because its value remains constant throughout its scope.
This rule is crucial because it prevents unexpected behavior when lambdas are executed later, such as in a different thread or after the method has returned. If a lambda could modify a captured variable, it could lead to unpredictable results, especially in concurrent programming scenarios.
Capturing Local Variables
This example demonstrates capturing a local variable in a lambda expression. The variable must be effectively final to be captured.
void main() { final String greeting = "Hello"; Runnable r = () -> System.out.println(greeting + " there!"); r.run(); }
Here, the lambda captures the local variable 'greeting' which is explicitly declared as final. The lambda can access this variable when it executes. If we tried to modify greeting after the lambda declaration, it would cause a compilation error.
$ java Main.java Hello there!
Effectively Final Variables
This example shows that variables don't need to be explicitly final if they're effectively final. The compiler treats them as final if they're not modified.
void main() { int count = 5; // effectively final Function<Integer, Integer> multiplier = x -> x * count; System.out.println(multiplier.apply(3)); }
The variable count
is effectively final because it's not modified
after initialization. The lambda captures this variable and uses it in the
multiplication. Attempting to modify count after the lambda would cause an
error.
$ java Main.java 15
Instance Variable Capture
Lambda expressions can capture instance variables without any restrictions. Unlike local variables, instance variables don't need to be final or effectively final.
class Example { private String prefix = "Result: "; void process() { Function<Integer, String> formatter = n -> prefix + n; System.out.println(formatter.apply(42)); } } void main() { new Example().process(); }
The lambda captures the instance variable prefix
which can be
modified even after the lambda declaration. This is because instance variables
are stored in the heap, while local variables are stored in the stack. The
lambda can access the instance variable directly without any restrictions.
Lambdas in Java can freely access and modify instance variables (like
prefix
) because these variables are stored in the heap as part of
the object's state. When a lambda references an instance variable, it implicitly
captures the enclosing object (this
), allowing it to access the
variable's current value at any timeāeven if the variable changes after the
lambda is declared. For example, if prefix is updated later, the lambda will
reflect the latest value when executed, since it directly references the
object's field on the heap. This dynamic access is safe because the object (and
its fields) exist as long as the lambda does.
In contrast, local variables are stored on the stack and exist only while the method is executing. To prevent concurrency issues or access to invalid memory, Java requires lambdas to treat local variables as effectively final (unchanged after assignment). When a lambda uses a local variable, Java creates a copy of its value at the time the lambda is defined. This ensures the lambda has a consistent snapshot, even if the original variable's stack frame is destroyed. Modifying the local variable after the lambda's creation would create a mismatch between the copied value and the original, so the compiler enforces immutability to maintain correctness.
$ java Main.java Result: 42
Capturing Method Parameters
Method parameters can be captured by lambda expressions if they are effectively final. This example demonstrates capturing a method parameter in a lambda.
void printFiltered(List<String> list, String word) { list.stream() .filter(s -> s.contains(word)) // capturing method parameter .forEach(System.out::println); } void main() { printFiltered(List.of("apple", "banana", "grape"), "ap"); }
The lambda captures the method parameter word
which is
effectively final. The parameter is used in the word operation to select
strings containing the word text. The parameter cannot be modified after being
captured.
$ java Main.java apple grape
Attempting to Modify Captured Variable
This example demonstrates what happens when you try to modify a captured variable, which violates the effectively final requirement.
void main() { int counter = 0; // This lambda would cause a compilation error if uncommented // Runnable incrementer = () -> counter++; // Error // counter++; // This would make counter not effectively final Runnable printer = () -> System.out.println("Counter: " + counter); printer.run(); }
The commented code shows an attempt to modify a captured variable, which would cause a compilation error. The working example only reads the variable, which is allowed as long as the variable remains effectively final.
$ java Main.java Counter: 0
Capturing Multiple Variables
Lambdas can capture multiple variables from their enclosing scope, as long as all captured variables are effectively final.
void main() { String name = "Alice"; int age = 30; String format = "%s is %d years old"; Supplier<String> info = () -> String.format(format, name, age); System.out.println(info.get()); }
This lambda captures three variables: name, age, and format. All are effectively final and can be used within the lambda body. The lambda combines these values to create a formatted string.
$ java Main.java Alice is 30 years old
Capturing in Nested Lambdas
This example demonstrates variable capture in nested lambda expressions. The same effectively final rules apply to variables captured by nested lambdas.
void main() { final int base = 10; Function<Integer, Function<Integer, Integer>> adderCreator = x -> y -> x + y + base; // nested lambda Function<Integer, Integer> addFive = adderCreator.apply(5); System.out.println(addFive.apply(3)); // 5 + 3 + 10 = 18 }
The outer lambda captures 'base' and the inner lambda captures x
from the outer lambda. Both variables must be effectively final. The result is a
function that adds three numbers together in a curried fashion.
$ java Main.java 18
Source
Java Language Specification - Lambda Expressions
This tutorial has explored variable capture in Java lambda expressions. We've seen how lambdas can capture local variables, instance variables, and method parameters, with the important restriction that local variables must be effectively final. These rules help ensure predictable behavior in functional programming contexts.
Author
List all Java tutorials.