Java "The blank final field may not have been initialized" 匿名接口与 Lambda 表达式
Java "The blank final field may not have been initialized" Anonymous Interface vs Lambda Expression
我最近遇到了错误消息 "The blank final field obj may not have been initialized"。
如果您尝试引用可能尚未分配给值的字段,通常就是这种情况。例子 class:
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 表达式:
public class Foo {
private final Object obj;
public Foo() {
Runnable run = () -> {
obj.toString(); // error
};
obj = new Object();
obj.toString(); // works again
}
}
这是我无法再关注的地方。在这里,我再次收到警告。我知道编译器不会像通常的初始化那样处理 lambda 表达式,它不会 "replace it by the long version"。但是,为什么这会影响我在创建 Runnable
对象时 运行 我的 run()
方法中的代码部分这一事实?我仍然能够在 调用 run()
之前进行初始化 。所以从技术上讲,这里可能不会遇到 NullPointerException
。 (虽然在这里也检查 null
会更好。但这个约定是另一个话题。)
我犯了什么错误? lambda 的处理方式有何不同,以至于它会影响我的对象使用方式?
感谢您的进一步解释。
我无法使用 Eclipse 的编译器重现你最后一个案例的错误。
但是,我可以想象的 Oracle 编译器的推理如下:在 lambda 内部,必须在声明时捕获 obj
的值。即在lambda体内声明时必须初始化。
但是,在这种情况下,Java 应该捕获 Foo
实例的值而不是 obj
。然后它可以通过(已初始化的)Foo
对象引用访问 obj
并调用其方法。这就是 Eclipse 编译器编译您的代码的方式。
规范中对此有所暗示,here:
The timing of method reference expression evaluation is more complex
than that of lambda expressions (§15.27.4). When a method reference
expression has an expression (rather than a type) preceding the ::
separator, that subexpression is evaluated immediately. The result of
evaluation is stored until the method of the corresponding functional
interface type is invoked; at that point, the result is used as the
target reference for the invocation. This means the expression
preceding the :: separator is evaluated only when the program
encounters the method reference expression, and is not re-evaluated on
subsequent invocations on the functional interface type.
类似的事情发生在
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
时已完全初始化。
你可以通过
绕过这个问题
Runnable run = () -> {
(this).obj.toString();
};
这在 lambda 开发期间讨论过,基本上在 明确赋值分析 期间将 lambda 主体视为本地代码。
引用 Dan Smith,spec tzar,https://bugs.openjdk.java.net/browse/JDK-8024809
The rules carve out two exceptions: ... ii) a use from inside of an anonymous class is okay. There is no exception for a use inside of a lambda expression
坦率地说,我和其他一些人认为这个决定是错误的。 lambda 仅捕获 this
,而不捕获 obj
。这种情况应该与匿名 class 一样对待。当前的行为对于许多合法用例来说是有问题的。好吧,你总是可以使用上面的技巧绕过它 - 幸运的是
定性赋值分析不太聪明,我们可以糊弄它。
您可以使用实用程序方法强制捕获 this
。这也适用于 Java 9。
public static <T> T r(T object) {
return object;
}
现在,您可以像这样重写您的 lambda:
Runnable run = () -> r(this).obj.toString();
我遇到了类似的问题:
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();
另一个解决方案是使用 getter。在我的示例中,它看起来像这样:
public Supplier<Object> sup = () -> getObj();
private Object getObj() {
return obj;
}
我最近遇到了错误消息 "The blank final field obj may not have been initialized"。
如果您尝试引用可能尚未分配给值的字段,通常就是这种情况。例子 class:
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 表达式:
public class Foo {
private final Object obj;
public Foo() {
Runnable run = () -> {
obj.toString(); // error
};
obj = new Object();
obj.toString(); // works again
}
}
这是我无法再关注的地方。在这里,我再次收到警告。我知道编译器不会像通常的初始化那样处理 lambda 表达式,它不会 "replace it by the long version"。但是,为什么这会影响我在创建
我犯了什么错误? lambda 的处理方式有何不同,以至于它会影响我的对象使用方式?
感谢您的进一步解释。Runnable
对象时 运行 我的 run()
方法中的代码部分这一事实?我仍然能够在 调用 run()
之前进行初始化 。所以从技术上讲,这里可能不会遇到 NullPointerException
。 (虽然在这里也检查 null
会更好。但这个约定是另一个话题。)
我无法使用 Eclipse 的编译器重现你最后一个案例的错误。
但是,我可以想象的 Oracle 编译器的推理如下:在 lambda 内部,必须在声明时捕获 obj
的值。即在lambda体内声明时必须初始化。
但是,在这种情况下,Java 应该捕获 Foo
实例的值而不是 obj
。然后它可以通过(已初始化的)Foo
对象引用访问 obj
并调用其方法。这就是 Eclipse 编译器编译您的代码的方式。
规范中对此有所暗示,here:
The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.
类似的事情发生在
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
时已完全初始化。
你可以通过
绕过这个问题 Runnable run = () -> {
(this).obj.toString();
};
这在 lambda 开发期间讨论过,基本上在 明确赋值分析 期间将 lambda 主体视为本地代码。
引用 Dan Smith,spec tzar,https://bugs.openjdk.java.net/browse/JDK-8024809
The rules carve out two exceptions: ... ii) a use from inside of an anonymous class is okay. There is no exception for a use inside of a lambda expression
坦率地说,我和其他一些人认为这个决定是错误的。 lambda 仅捕获 this
,而不捕获 obj
。这种情况应该与匿名 class 一样对待。当前的行为对于许多合法用例来说是有问题的。好吧,你总是可以使用上面的技巧绕过它 - 幸运的是
定性赋值分析不太聪明,我们可以糊弄它。
您可以使用实用程序方法强制捕获 this
。这也适用于 Java 9。
public static <T> T r(T object) {
return object;
}
现在,您可以像这样重写您的 lambda:
Runnable run = () -> r(this).obj.toString();
我遇到了类似的问题:
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();
另一个解决方案是使用 getter。在我的示例中,它看起来像这样:
public Supplier<Object> sup = () -> getObj();
private Object getObj() {
return obj;
}