Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
ZetCode

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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.