Java“空白的最终字段可能尚未初始化”Anonymous Interface vs Lambda Expression

我最近遇到了错误消息“空白的最终字段obj可能尚未初始化”。

通常情况下,如果您尝试引用可能尚未分配给值的字段。 示例类:

public class Foo { private final Object obj; public Foo() { obj.toString(); // error (1) obj = new Object(); obj.toString(); // just fine (2) } } 

我用Eclipse。 在第(1)我得到错误,在第(2)一切正常。 到目前为止这是有道理的。

接下来,我尝试在构造函数内创建的匿名接口中访问obj

 public class Foo { private Object obj; public Foo() { Runnable run = new Runnable() { public void run() { obj.toString(); // works fine } }; obj = new Object(); obj.toString(); // works too } } 

这也有效,因为我在创建界面时不会访问obj 。 我也可以将我的实例传递给其他地方,然后初始化对象obj然后运行我的界面。 (但是在使用之前检查null是合适的)。 仍然有道理。

但是现在我通过使用lambda表达式 将我的Runnable实例的创建缩短为汉堡箭头版本:

 public class Foo { private final Object obj; public Foo() { Runnable run = () -> { obj.toString(); // error }; obj = new Object(); obj.toString(); // works again } } 

这是我不能再追随的地方了。 我再次收到警告。 我知道编译器不像通常的初始化那样处理lambda表达式,它不会“用长版本替换它”。 但是,为什么这会影响我在Runnable对象创建时不在run()方法中运行代码部分的事实? 调用run() 之前,我仍然可以进行初始化。 所以从技术上讲,这里可能不会遇到NullPointerException 。 (虽然最好在这里检查一下null 。但是这个约定是另一个话题。)

我犯的错是什么? lambda的处理方式如此不同,它会影响我的对象使用方式吗?

我感谢你的任何进一步解释。

我无法使用Eclipse的编译器为您的最终案例重现错误。

但是,我可以想象的Oracle编译器的原因如下:在lambda中,必须在声明时捕获obj的值。 也就是说,它必须在lambda体内声明时初始化。

但是,在这种情况下,Java应该捕获Foo实例的值而不是obj 。 然后它可以通过(初始化的) Foo对象引用访问obj并调用其方法。 这就是Eclipse编译器编译代码的方式。

这在说明书中暗示, 这里 :

方法参考表达式评估的时间比lambda表达式(第15.27.4节)更复杂。 当方法引用表达式在:: separator之前具有表达式(而不是类型)时,将立即计算该子表达式。 存储评估结果,直到调用相应function接口类型的方法为止 ; 此时,结果将用作调用的目标引用。 这意味着:: separator之前的表达式仅在程序遇到方法引用表达式时计算,并且不会在函数接口类型的后续调用中重新计算。

类似的事情发生了

 Object obj = new Object(); // imagine some local variable Runnable run = () -> { obj.toString(); }; 

想象一下obj是一个局部变量,当执行lambda表达式代码时, obj被计算并产生一个引用。 此引用存储在创建的Runnable实例的字段中。 调用run.run() ,实例使用存储的引用值。

如果未初始化obj则不会发生这种情况。 例如

 Object obj; // imagine some local variable Runnable run = () -> { obj.toString(); // error }; 

lambda无法捕获obj的值,因为它还没有值。 它实际上相当于

 final Object anonymous = obj; // won't work if obj isn't initialized Runnable run = new AnonymousRunnable(anonymous); ... class AnonymousRunnable implements Runnable { public AnonymousRunnable(Object val) { this.someHiddenRef = val; } private final Object someHiddenRef; public void run() { someHiddenRef.toString(); } } 

这就是Oracle编译器当前对您的代码段的行为方式。

但是,Eclipse编译器不是捕获obj的值,而是捕获它的值( Foo实例)。 它实际上相当于

 final Foo anonymous = Foo.this; // you're in the Foo constructor so this is valid reference to a Foo instance Runnable run = new AnonymousRunnable(anonymous); ... class AnonymousRunnable implements Runnable { public AnonymousRunnable(Foo foo) { this.someHiddenRef = foo; } private final Foo someHiddenFoo; public void run() { someHiddenFoo.obj.toString(); } } 

这很好,因为您假设在调用run完全初始化了Foo实例。

你可以绕过这个问题

  Runnable run = () -> { (this).obj.toString(); }; 

这在lambda开发期间进行了讨论,基本上在明确的赋值分析期间将lambda体视为局部代码。

引用Dan Smith,spec tzar, https: //bugs.openjdk.java.net/browse/JDK-8024809

规则划出两个例外:… ii)从匿名类内部使用是可以的。 在lambda表达式中使用没有例外

坦率地说,我和其他一些人认为这个决定是错误的。 lambda只捕获this ,而不是obj 。 此案例应该与匿名类一样对待。 对于许多合法用例,当前的行为是有问题的。 好吧,你总是可以使用上面的技巧绕过它 – 幸运的是, 明确的分配分析不是太聪明,我们可以欺骗它。