Java“空白的最终字段可能尚未初始化”匿名接口与 Lambda 表达式
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/30360824/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java "The blank final field may not have been initialized" Anonymous Interface vs Lambda Expression
提问by Steffen T
I've recently been encountering the error message "The blank final field obj may not have been initialized".
我最近遇到错误消息“空白的最终字段 obj 可能尚未初始化”。
Usually this is the case if you try to refer to a field that is possibly not assigned to a value yet. Example class:
如果您尝试引用可能尚未分配给值的字段,通常就是这种情况。示例类:
public class Foo {
private final Object obj;
public Foo() {
obj.toString(); // error (1)
obj = new Object();
obj.toString(); // just fine (2)
}
}
I use Eclipse. In the line (1)
I get the error, in the line (2)
everything works. So far that makes sense.
我使用 Eclipse。在(1)
我收到错误的那一行中,在该行中(2)
一切正常。到目前为止,这是有道理的。
Next I try to access obj
within an anonymous interface I create inside the constructor.
接下来,我尝试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
}
}
This works, too, since I do not access obj
in the moment I create the interface. I could also pass my instance to somewhere else, then initialize the object obj
and then run my interface. (However it would be appropriate to check for null
before using it). Still makes sense.
这也有效,因为我obj
在创建界面的那一刻没有访问。我也可以将我的实例传递到其他地方,然后初始化对象obj
,然后运行我的界面。(但是null
在使用之前检查一下是合适的)。还是有道理的。
But now I shorten the creation of my Runnable
instance tothe burger-arrow version by using a lambda expression:
但是现在我Runnable
通过使用lambda 表达式将我的实例的创建缩短为汉堡箭头版本:
public class Foo {
private final Object obj;
public Foo() {
Runnable run = () -> {
obj.toString(); // error
};
obj = new Object();
obj.toString(); // works again
}
}
And here is where I can't follow anymore. Here I get the warning again. I am aware that the compiler doesn't handle lambda expressions as usual initializations, it doesn't "replace it by the long version". However, why does this affect the fact that I do not run the code part in my run()
method at creation time of the Runnable
object? I am still able to do the initialization beforeI invoke run()
. So technically it is possible not to encounter a NullPointerException
here. (Though it would be better to check for null
here, too. But this convention is another topic.)
这是我不能再跟随的地方。在这里,我再次收到警告。我知道编译器不像通常的初始化那样处理 lambda 表达式,它不会“用长版本替换它”。但是,为什么这会影响我run()
在创建Runnable
对象时不在我的方法中运行代码部分的事实?我仍然可以在调用之前进行初始化run()
。所以从技术上讲,NullPointerException
在这里不遇到 a 是可能的。(虽然最好null
也在这里检查。但这个约定是另一个主题。)
What is the mistake I make? What is handled so differently about lambda that it influences my object usage the way it does?
我犯了什么错误?lambda 的处理方式有何不同,以至于它会以这种方式影响我的对象使用?
I thank you for any further explanations.
我感谢您的任何进一步解释。
采纳答案by Sotirios Delimanolis
I can't reproduce the error for your final case with Eclipse's compiler.
我无法使用 Eclipse 的编译器为您的最终案例重现错误。
However, the reasoning for the Oracle compiler I can imagine is the following: inside a lambda, the value of obj
must be captured at declaration time. That is, it must be initialized when it is declared inside the lambda body.
但是,我可以想象的 Oracle 编译器的推理如下:在 lambda 中,obj
必须在声明时捕获的值。也就是说,它必须在 lambda 体内声明时进行初始化。
But, in this case, Java should capture the value of the Foo
instance rather than obj
. It can then access obj
through the (initialized) Foo
object reference and invoke its method. This is how the Eclipse compiler compiles your piece of code.
但是,在这种情况下,Java 应该捕获Foo
实例的值而不是obj
. 然后它可以obj
通过(初始化的)Foo
对象引用访问并调用它的方法。这就是 Eclipse 编译器编译您的代码的方式。
This is hinted at in the specification, 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.
方法引用表达式评估的时间比 lambda 表达式(第 15.27.4 节)更复杂。当方法引用表达式在 :: 分隔符之前有一个表达式(而不是一个类型)时,该子表达式会被立即计算。求值的结果一直保存到对应的函数接口类型的方法被调用;此时,结果将用作调用的目标引用。这意味着 :: 分隔符之前的表达式仅在程序遇到方法引用表达式时才计算,并且不会在功能接口类型的后续调用中重新计算。
A similar thing happens for
类似的事情发生在
Object obj = new Object(); // imagine some local variable
Runnable run = () -> {
obj.toString();
};
Imagine obj
is a local variable, when the lambda expression code is executed, obj
is evaluated and produces a reference. This reference is stored in a field in the Runnable
instance created. When run.run()
is called, the instance uses the reference value stored.
Imagineobj
是一个局部变量,当 lambda 表达式代码被执行时,obj
被评估并产生一个引用。此引用存储在Runnable
创建的实例中的字段中。当run.run()
被调用时,例如使用存储的参考值。
This cannot happen if obj
isn't initialized. For example
如果obj
未初始化,则不会发生这种情况。例如
Object obj; // imagine some local variable
Runnable run = () -> {
obj.toString(); // error
};
The lambda cannot capture the value of obj
, because it doesn't have a value yet. It's effectively equivalent to
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();
}
}
This is how the Oracle compiler is currently behaving for your snippet.
这就是 Oracle 编译器当前对您的代码段的行为方式。
However, the Eclipse compiler is, instead, not capturing the value of obj
, it's capturing the value of this
(the Foo
instance). It's effectively equivalent to
但是,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();
}
}
Which is fine because you assume that the Foo
instance is completely initialized by the time run
is invoked.
这很好,因为您假设Foo
实例在run
调用时已完全初始化。
回答by ZhongYu
You can bypass the problem by
您可以通过以下方式绕过问题
Runnable run = () -> {
(this).obj.toString();
};
This was discussed during lambda development, basically the lambda body is treated as local code during definite assignment analysis.
这在 lambda 开发期间讨论过,基本上 lambda 主体在确定分配分析期间被视为本地代码。
Quoting Dan Smith, spec tzar, https://bugs.openjdk.java.net/browse/JDK-8024809
引用 Dan Smith,spec tsar,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
规则排除了两个例外:... ii) 从匿名类内部使用是可以的。在 lambda 表达式内部使用也不例外
Frankly I and some other people thought the decision is wrong. The lambda only captures this
, not obj
. This case should have been treated the same as anonymous class. The current behavior is problematic for many legit use cases . Well, you can always bypass it using the trick above- fortunately
definite assignment analysisis not too smart and we can fool it.
坦率地说,我和其他一些人认为这个决定是错误的。lambda 只捕获this
,而不捕获obj
。这种情况应该与匿名类一样对待。对于许多合法用例,当前的行为是有问题的。好吧,你总是可以使用上面的技巧绕过它——幸运的是,
确定分配分析不是太聪明,我们可以骗过它。
回答by Dávid Horváth
You can use a utility method to force capturing this
only. This works with Java 9 too.
您可以使用实用程序方法this
仅强制捕获。这也适用于 Java 9。
public static <T> T r(T object) {
return object;
}
Now, you can rewrite your lambda like this:
现在,您可以像这样重写您的 lambda:
Runnable run = () -> r(this).obj.toString();
回答by John McClane
I had a similar problem:
我有一个类似的问题:
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;
}
}
And resolved it this way:
并以这种方式解决它:
public Supplier<Object> sup = () -> ((ObjectHolder)this).obj;
Neither this.obj
nor ObjectHolder.this.obj
worked in Eclipse (though the latter worked for the standard JDK compiler).
在 Eclipse 中既不工作this.obj
也不ObjectHolder.this.obj
工作(尽管后者适用于标准 JDK 编译器)。
In your case, use this workaround, it is safe for all compilers:
在您的情况下,请使用此解决方法,它对所有编译器都是安全的:
((Foo)this).obj.toString();
Another solution is to use a getter. In my example, it looks like this:
另一种解决方案是使用吸气剂。在我的示例中,它看起来像这样:
public Supplier<Object> sup = () -> getObj();
private Object getObj() {
return obj;
}