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

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

 public class TestClass { public static void main(String[] args) { MyClass m = new MyClass(); Consumer fn = m.getConsumer(); System.out.println("Just to put a breakpoint"); } } class MyClass { final String foo = "foo"; public Consumer getConsumer() { return bar -> System.out.println(bar + foo); } } 

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

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

在此处输入图像描述

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

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

输出:

 bar1null bar2foo 

如果foo被lambda捕获,它将被捕获为null ,第二个调用将打印bar2null 。 但是,由于捕获了MyClass实例,因此会打印正确的值。

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

请注意,唯一真正丑陋的是,我们通过消费者强制读取构造函数中要分配的foo 。 建立消费者本身并不期望在那时阅读foo ,因此在分配foo之前构建它仍然是合法的 – 只要你不立即使用它。

但是编译器在分配foo之前不会让你在构造函数中初始化相同的consumer – 可能是最好的:-)

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

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

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

你是对的,它在技术上可以这样做,因为有问题的领域是final ,但事实并非如此。

但是,如果返回的lambda保留对MyClass实例的引用是一个问题,那么您可以自己轻松修复它:

 public Consumer getConsumer() { String f = this.foo; return bar -> System.out.println(bar + f); } 

注意,如果字段不是final字段,那么原始代码将使用lambda执行时的实际值,而此处列出的代码将使用截至执行getConsumer()方法时的值。

请注意,对于作为编译时常量的变量的任何普通Java访问,都会发生常量值,因此,与某些人声称的不同,它不受初始化顺序问题的影响。

我们可以通过以下示例演示:

 abstract class Base { Base() { // bad coding style don't do this in real code printValues(); } void printValues() { System.out.println("var1 read: "+getVar1()); System.out.println("var2 read: "+getVar2()); System.out.println("var1 via lambda: "+supplier1().get()); System.out.println("var2 via lambda: "+supplier2().get()); } abstract String getVar1(); abstract String getVar2(); abstract Supplier supplier1(); abstract Supplier supplier2(); } public class ConstantInitialization extends Base { final String realConstant = "a constant"; final String justFinalVar; { justFinalVar = "a final value"; } ConstantInitialization() { System.out.println("after initialization:"); printValues(); } @Override String getVar1() { return realConstant; } @Override String getVar2() { return justFinalVar; } @Override Supplier supplier1() { return () -> realConstant; } @Override Supplier supplier2() { return () -> justFinalVar; } public static void main(String[] args) { new ConstantInitialization(); } } 

它打印:

 var1 read: a constant var2 read: null var1 via lambda: a constant var2 via lambda: null after initialization: var1 read: a constant var2 read: a final value var1 via lambda: a constant var2 via lambda: a final value 

因此,正如您所看到的,当执行超级构造函数时,对realConstant字段的写入尚未发生,即使通过lambda表达式访问它,也看不到真正的编译时常量的未初始化值。 从技术上讲,因为该字段实际上并未被读取。

此外,出于同样的原因,讨厌的Reflection hacks对普通Java访问编译时常量没有影响。 读回这样一个修改后的值的唯一方法是通过Reflection:

 public class TestCapture { static class MyClass { final String foo = "foo"; private Consumer getFn() { //final String localFoo = foo; return bar -> System.out.println("lambda: " + bar + foo); } } public static void main(String[] args) throws ReflectiveOperationException { final MyClass obj = new MyClass(); Consumer fn = obj.getFn(); // change the final field obj.foo Field foo=obj.getClass().getDeclaredFields()[0]; foo.setAccessible(true); foo.set(obj, "bar"); // prove that our lambda expression doesn't read the modified foo fn.accept(""); // show that it captured obj Field capturedThis=fn.getClass().getDeclaredFields()[0]; capturedThis.setAccessible(true); System.out.println("captured obj: "+(obj==capturedThis.get(fn))); // and obj.foo contains "bar" when actually read System.out.println("via Reflection: "+foo.get(capturedThis.get(fn))); // but no ordinary Java access will actually read it System.out.println("ordinary field access: "+obj.foo); } } 

它打印:

 lambda: foo captured obj: true via Reflection: bar ordinary field access: foo 

这向我们展示了两件事,

  1. reflection也对编译时常量没有影响
  2. 尽管不会使用,但已捕获了周围的物体

我很乐意找到一个解释,比如“对实例字段的任何访问都需要 lambda表达式来捕获该字段的实例(即使实际上没有读取该字段)”,但不幸的是我找不到任何语句关于在当前Java语言规范中捕获值或this ,这有点可怕:

我们已经习惯了这样一个事实: 访问lambda表达式中的实例字段会创建一个没有对此引用的实例,但即使这样也不是当前规范所保证的。 重要的是,这个遗漏很快得到修复……