我最近遇到了错误消息“空白的最终字段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表达式,它不会“用长版本替换它”。但是,为什么这会影响我在run()
对象创建时不在Runnable
方法中运行代码部分的事实?我仍然可以在调用run()
之前进行初始化。从技术上讲,这里可能不会遇到NullPointerException
。 (虽然最好在这里检查一下null
。但这个约定是另一个话题。)
我犯的错是什么? lambda的处理方式如此不同,它会影响我的对象使用方式吗?
我感谢您的任何进一步解释。
答案 0 :(得分:15)
您可以通过
绕过问题 Runnable run = () -> {
(this).obj.toString();
};
在lambda开发过程中对此进行了讨论,基本上在明确赋值分析期间将lambda主体视为本地代码。
引用Dan Smith,spec tzar,https://bugs.openjdk.java.net/browse/JDK-8024809
坦率地说,我和其他一些人认为这个决定是错误的。 lambda只捕获规则划出两个例外:... ii)从匿名类内部使用是可以的。在lambda表达式
中使用没有例外
this
,而不是obj
。这个案例应该被视为匿名类。对于许多合法用例,当前的行为是有问题的。好吧,你总是可以使用上面的技巧绕过它 - 幸运的是
明确的分配分析不太聪明,我们可以欺骗它。
答案 1 :(得分:10)
我无法使用Eclipse的编译器为您的最终案例重现错误。
但是,我可以想象的Oracle编译器的原因如下:在lambda中,必须在声明时捕获obj
的值。也就是说,它必须在lambda体内声明时初始化。
但是,在这种情况下,Java应该捕获Foo
实例的值而不是obj
。然后,它可以通过(初始化的)obj
对象引用访问Foo
并调用其方法。这就是Eclipse编译器编译代码的方式。
在规范here中暗示了这一点:
方法参考表达式评估的时间更复杂 而不是lambda表达式(§15.27.4)。当一个方法参考 expression在::之前有一个表达式(而不是一个类型) 分隔符,立即计算子表达式。 结果 评估存储直到相应功能的方法 接口类型被调用;在那一点上,结果被用作 调用的目标引用。这意味着表达 在:: 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
的值,而是捕获this
(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();
}
}
这很好,因为您假设Foo
实例在调用时间run
时已完全初始化。
答案 2 :(得分:0)
您可以使用实用程序方法仅强制捕获this
。这也适用于Java 9。
public static <T> T r(T object) {
return object;
}
现在,您可以像这样重写lambda:
Runnable run = () -> r(this).obj.toString();
答案 3 :(得分:0)
我有一个类似的问题:
import java.util.function.Supplier;
public class ObjectHolder {
private final Object obj;
public Supplier<Object> sup = () -> obj; // error
public ObjectHolder(Object obj) {
this.obj = obj;
}
}
并通过以下方式解决该问题:
public Supplier<Object> sup = () -> ((ObjectHolder)this).obj;
this.obj
和ObjectHolder.this.obj
都不在Eclipse中工作(尽管后者在标准JDK编译器中工作)。
对于您而言,使用此替代方法,对于所有编译器都是安全的:
((Foo)this).obj.toString();
另一种解决方案是使用吸气剂。在我的示例中,它看起来像这样:
public Supplier<Object> sup = () -> getObj();
private Object getObj() {
return obj;
}