Lambda 表达式和变量捕获

2022-09-03 14:36:10

请向我解释 lambda 表达式如何使用和修改其封闭类的实例变量,但只能使用其封闭作用域的局部变量。(除非是最终或有效的最终决定?

我的基本问题是,在作用域的上下文中,类的实例变量如何从 lambda 中可修改,而局部变量则不能。


答案 1

首先,我们可以看一下JLS,它声明如下:

在 lambda 表达式中使用但未声明的任何局部变量、形式参数或异常参数都必须声明为 final 或实际上是 final (§4.12.4),否则在尝试使用时会发生编译时错误。

在 lambda 主体中使用但未声明的任何局部变量都必须在 lambda 主体之前明确赋值 (§16(定赋值)),否则会发生编译时错误。

关于变量使用的类似规则也适用于内部类的主体 (§8.1.3)。对有效最终变量的限制禁止访问动态变化的局部变量,这些局部变量的捕获可能会引入并发问题。与最终的限制相比,它减轻了程序员的文书负担。

对有效最终变量的限制包括标准循环变量,但不包括增强型循环变量,对于循环的每次迭代,这些变量被视为不同 (§14.14.2)。


为了更好地理解它,请看一下这个示例类:

public class LambdaTest {

    public static void main(String[] args) {
        LambdaTest test = new LambdaTest();
        test.returnConsumer().accept("Hello");
        test.returnConsumerWithInstanceVariable().accept("Hello");
        test.returnConsumerWithLocalFinalVariable().accept("Hello");
    }

    String string = " world!";

    Consumer<String> returnConsumer() {
        return ((s) -> {System.out.println(s);});
    }

    Consumer<String> returnConsumerWithInstanceVariable() {
        return ((s) -> {System.out.println(s + string);});
    }

    Consumer<String> returnConsumerWithLocalFinalVariable() {
        final String foo = " you there!";
        return ((s) -> {System.out.println(s + foo);});
    }

}

主要输出为

Hello
Hello world!
Hello you there!

这是因为在此处返回 lambda 与使用 创建一个新的匿名类大致相同。您的 lambda - 的实例具有对创建它的类的引用。你可以重写 to use ,这将执行完全相同的操作。这就是允许您访问(和修改)实例变量的原因。new Consumer<String>() {...}Consumer<String>returnConsumerWithInstanceVariable()System.out.println(s + LambdaTest.this.string)

如果您的方法中有一个(有效的)最终局部变量,则可以访问它,因为它会复制到您的 lambda 实例。

但是,如果它不是最终的,您认为在以下上下文中应该发生什么:

Consumer<String> returnConsumerBad() {
    String foo = " you there!";
    Consumer<String> results = ((s) -> {System.out.println(s + foo);});
    foo = " to all of you!";
    return results;
}

是否应将值复制到您的实例,但在更新局部变量时不更新该值?这可能会引起混淆,因为我认为许多程序员在返回此lambda后会期望“给所有人”新值。foo

如果你有一个基元值,它将位于堆栈上。因此,您不能简单地引用局部变量,因为它可能会在到达方法的末尾后消失。


答案 2

您可以参考本文 - https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood lambda 表达式编译中介绍的内容。如前所述,lambda表达式/代码块被编译到匿名类中,这些匿名类是用名称格式()编译的,所以假设如果允许非最终局部变量,那么编译器就无法跟踪它,这个局部变量被称为匿名类''.class'文件是像普通的java类一样分别使用上述格式创建/编译的。<<Enclosing Class name>>$<<1(Number)>>

因此,如果局部变量是最终的,那么编译器会在 anoymous 类中创建一个最终实例,这不会给编译器带来歧义。有关详细信息,请参阅上面提到的链接