为什么lambda在引用最终的String字段时需要捕获封闭的实例?

man*_*uti 12 java string lambda java-8

这是基于这个问题.考虑这个示例,其中方法Consumer基于lambda表达式返回:

public class TestClass {
    public static void main(String[] args) {
        MyClass m = new MyClass();
        Consumer<String> fn = m.getConsumer();

        System.out.println("Just to put a breakpoint");
    }
}

class MyClass {
    final String foo = "foo";

    public Consumer<String> getConsumer() {
        return bar -> System.out.println(bar + foo);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们知道,在进行函数式编程时引用lambda中的当前状态并不是一个好习惯,原因之一是lambda将捕获封闭的实例,在lambda本身超出范围之前不会进行垃圾收集.

但是,在这个与final字符串相关的特定场景中,似乎编译器可能只是在返回的lambda中包含了constant(final)字符串foo(来自常量池),而不是MyClass在调试时将所有实例括起来,如下所示(放置在的System.out.println).是否与lambdas编译为特殊invokedynamic字节码的方式有关?

在此输入图像描述

Bri*_*etz 14

在你的代码中,bar + foo真的是简写bar + this.foo; 我们只是习惯了速记,忘记了我们隐含地获取了一个实例成员.所以你的lambda正在捕获this,而不是this.foo.

如果你的问题是"这个功能是否能以不同的方式实现",答案是"可能是的"; 我们本可以使lambda捕获的规范/实现更加复杂,目的是为各种特殊情况(包括这种情况)提供更好的性能.

更改规范以便我们捕获this.foo而不是this在性能方面不会发生太大变化; 它仍然是一个捕获lambda,这是一个比额外的字段解引用更大的成本考虑.所以我认为这不会提供真正的性能提升.


Did*_*r L 3

如果 lambda 捕获foo而不是this,则在某些情况下可能会得到不同的结果。考虑以下示例:

\n\n
public class TestClass {\n    public static void main(String[] args) {\n        MyClass m = new MyClass();\n        m.consumer.accept("bar2");\n    }\n}\n\nclass MyClass {\n    final String foo;\n    final Consumer<String> consumer;\n\n    public MyClass() {\n        consumer = getConsumer();\n        // first call to illustrate the value that would have been captured\n        consumer.accept("bar1");\n        foo = "foo";\n    }\n\n    public Consumer<String> getConsumer() {\n        return bar -> System.out.println(bar + foo);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出:

\n\n
bar1null\nbar2foo\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果foo被 lambda 捕获,它将被捕获为null,第二次调用将打印bar2null。然而,由于MyClass实例被捕获,它会打印正确的值。

\n\n

当然,这是丑陋的代码并且有点做作,但在更复杂的现实代码中,这样的问题可能很容易发生

\n\n

请注意,唯一真正丑陋的事情是我们foo通过消费者强制读取构造函数中要分配的内容。foo构建消费者本身预计不会在那时读取,因此foo只要您不立即使用它,在分配 \xe2\x80\x93 之前构建它仍然是合法的。

\n\n

但是,在分配 \xe2\x80\x93 之前,编译器不会让您consumer在构造函数中初始化相同的内容foo,这可能是最好的:-)

\n