Lambda 表达式和变量捕获

Posted

技术标签:

【中文标题】Lambda 表达式和变量捕获【英文标题】:Lambda Expression and Variable Capture 【发布时间】:2015-11-23 05:07:24 【问题描述】:

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

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

【问题讨论】:

由于 lambda 实际上是一个匿名的内部类,这可能与 Why are only final variables accessible in anonymous class? 重复 这适用于 Java 8,但据我所知并没有具体说明,因此可能会在 Java 的未来版本中进行更改。 仅捕获this。所有对 lambda 体内实例变量的访问都是通过 this.field 完成的,因此您也可以对其进行写入。 @Alex - 我认为它是指定的,将来不会改变。见these comments。 @bayou.io - 虽然我很确定我之前读过一篇博文或其他内容,其中指出 Lambda 目前已编译为匿名内部类,但出于性能原因,这种情况是否会发生变化未来,我找不到这篇文章。所以也许你是对的 - 即使没有指定,目前它实际上是一样的。 【参考方案1】:

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

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

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

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

对有效最终变量的限制包括标准循环变量,但不包括增强型 for 循环变量,这些变量在循环的每次迭代中都被视为不同的变量(第 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););
    


main 的输出是

Hello
Hello world!
Hello you there!

这是因为在这里返回一个 lambda 与使用 new Consumer&lt;String&gt;() ... 创建一个新的匿名类非常相似。你的 lambda - Consumer&lt;String&gt; 的一个实例引用了它在其中创建的类。你可以重写 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;

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

如果你有一个原始值,它会放在堆栈上。所以你不能简单地引用局部变量,因为它可能会在你的方法结束后消失。

【讨论】:

【参考方案2】:

您可以参考这篇文章 - https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood 解释 lambda 表达式编译。正如解释的 lambda 表达式/代码块被编译到匿名类中,这些匿名类使用名称格式 (&lt;&lt;Enclosing Class name&gt;&gt;$&lt;&lt;1(Number)&gt;&gt;) 编译,因此假设假设如果允许非最终局部变量,则编译器无法从该局部变量的位置跟踪它被称为匿名类''.class'文件是用上述格式分别创建/编译的,就像普通的java类一样。

所以如果局部变量是最终的,那么编译器会在匿名类中创建一个最终实例,这不会给编译器造成歧义。 有关更多信息,请参阅上面提到的链接

【讨论】:

以上是关于Lambda 表达式和变量捕获的主要内容,如果未能解决你的问题,请参考以下文章

C++ lambda表达式(函数指针和function)

第13课 lambda表达式

Java 基础语法详解 Java 的 Lambda 表达式

Java 基础语法详解 Java 的 Lambda 表达式

Java 基础语法详解 Java 的 Lambda 表达式

如何在 lambda 表达式中捕获单个类数据成员?