ZetCode

Java Lambda 表达式中的变量捕获

最后修改于 2025 年 5 月 25 日

本教程探讨了 Java Lambda 表达式中的变量捕获,这是 Java 函数式编程中的一个基本概念。我们将介绍 lambda 与周围变量交互的规则和实际示例。

Lambda 表达式中的 变量捕获 是指 lambda 使用在其主体之外声明的变量的能力。这些变量被称为 lambda *捕获* 的。Java 对哪些变量可以被捕获以及如何使用它们有明确的规则。

最重要的规则是,在 lambda 中使用但未声明的任何局部变量、形式参数或异常参数必须是 final 的或事实上是 final 的。这确保了 lambda 执行时的可预测行为,尤其是在并发上下文中。

事实上是 final 的 变量是指在初始化后其值永远不会改变的变量。虽然没有用 final 关键字显式声明,但它可以被视为 final,因为它的值在其作用域内保持不变。

这条规则至关重要,因为它防止了 lambda 稍后执行时出现意外行为,例如在不同的线程中或在方法返回后。如果 lambda 可以修改捕获的变量,则可能导致不可预测的结果,尤其是在并发编程场景中。

捕获局部变量

此示例演示了在 lambda 表达式中捕获局部变量。该变量必须事实上是 final 的才能被捕获。

Main.java
void main() {

    final String greeting = "Hello";
    
    Runnable r = () -> System.out.println(greeting + " there!");
    r.run();
}

在这里,lambda 捕获了显式声明为 final 的局部变量 'greeting'。Lambda 可以在执行时访问此变量。 如果我们尝试在 lambda 声明后修改 greeting,则会导致编译错误。

$ java Main.java
Hello there!

事实上是 Final 的变量

这个例子表明,如果变量事实上是 final 的,则不需要显式地声明为 final。 如果它们没有被修改,编译器会将它们视为 final。

Main.java
void main() {

    int count = 5;  // effectively final
    
    Function<Integer, Integer> multiplier = x -> x * count;
    System.out.println(multiplier.apply(3));
}

变量 count 事实上是 final 的,因为它在初始化后没有被修改。 Lambda 捕获了这个变量并在乘法中使用它。 尝试在 lambda 之后修改 count 会导致错误。

$ java Main.java
15

实例变量捕获

Lambda 表达式可以捕获实例变量,没有任何限制。 与局部变量不同,实例变量不需要是 final 的或事实上是 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();
}

Lambda 捕获实例变量 prefix,即使在 lambda 声明之后也可以修改它。 这是因为实例变量存储在堆中,而局部变量存储在栈中。 Lambda 可以直接访问实例变量,没有任何限制。

Java 中的 Lambda 可以自由地访问和修改实例变量(如 prefix),因为这些变量作为对象状态的一部分存储在堆中。 当 lambda 引用实例变量时,它会隐式捕获封闭对象 (this),允许它随时访问变量的当前值,即使变量在 lambda 声明后发生更改。 例如,如果 prefix 稍后更新,lambda 将在执行时反映最新值,因为它直接引用堆上对象的字段。 这种动态访问是安全的,因为对象(及其字段)只要 lambda 存在就存在。

相比之下,局部变量存储在堆栈上,并且仅在方法执行时存在。 为了防止并发问题或访问无效内存,Java 要求 lambda 将局部变量视为事实上是 final 的(在赋值后保持不变)。 当 lambda 使用局部变量时,Java 会在定义 lambda 时创建其值的副本。 这确保了 lambda 具有一致的快照,即使原始变量的堆栈帧被销毁。 在 lambda 创建后修改局部变量会在复制的值和原始值之间创建不匹配,因此编译器强制执行不变性以保持正确性。

$ java Main.java
Result: 42

捕获方法参数

如果方法参数事实上是 final 的,则可以被 lambda 表达式捕获。 此示例演示了在 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");
}

Lambda 捕获方法参数 word,该参数事实上是 final 的。 该参数在 word 操作中用于选择包含单词文本的字符串。 捕获后不能修改该参数。

$ java Main.java
apple
grape

尝试修改捕获的变量

此示例演示了尝试修改捕获的变量时会发生什么,这违反了事实上是 final 的要求。

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();
}

注释掉的代码显示了修改捕获变量的尝试,这将导致编译错误。 工作示例仅读取变量,只要变量保持事实上是 final 的,这是允许的。

$ java Main.java
Counter: 0

捕获多个变量

Lambda 可以从其封闭范围捕获多个变量,只要所有捕获的变量都事实上是 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());
}

此 lambda 捕获三个变量:name、age 和 format。 所有这些变量都事实上是 final 的,并且可以在 lambda 主体中使用。 Lambda 结合这些值来创建一个格式化的字符串。

$ java Main.java
Alice is 30 years old

嵌套 Lambda 中的捕获

此示例演示了嵌套 lambda 表达式中的变量捕获。 相同的事实上是 final 的规则适用于嵌套 lambda 捕获的变量。

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
}

外部 lambda 捕获 'base',内部 lambda 从外部 lambda 捕获 x。 两个变量都必须事实上是 final 的。 结果是一个以柯里化方式将三个数字相加的函数。

$ java Main.java
18

来源

Java 语言规范 - Lambda 表达式

本教程探讨了 Java lambda 表达式中的变量捕获。 我们已经看到 lambda 如何捕获局部变量、实例变量和方法参数,但重要的是,局部变量必须事实上是 final 的。 这些规则有助于确保函数式编程上下文中可预测的行为。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程