Java“可能尚未初始化空白的最终字段"匿名接口与 Lambda 表达式

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

如果您尝试引用可能尚未分配值的字段,通常会出现这种情况.示例类:

I've recently been encountering the error message "The blank final field obj may not have been initialized".

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)
    }
}

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

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

I use Eclipse. In the line (1) I get the error, in the line (2) everything works. So far that makes sense.

Next I try to access obj within an anonymous interface I create inside the constructor.

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 实例的创建时间缩短为 burger-arrow 版本:

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.

But now I shorten the creation of my Runnable instance to the burger-arrow version by using a lambda expression:

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 的处理方式有何不同,以至于它会影响我的对象使用方式?

感谢您提供任何进一步的解释.

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 before I 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.)

What is the mistake I make? What is handled so differently about lambda that it influences my object usage the way it does?

I thank you for any further explanations.

推荐答案

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

I can't reproduce the error for your final case with Eclipse's compiler.

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

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.

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

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.

规范中暗示了这一点,这里:

This is hinted at in the specification, here:

方法引用表达式求值的时机更复杂比 lambda 表达式(第 15.27.4 节).当一个方法引用表达式在 :: 之前有一个表达式(而不是一个类型)分隔符,立即计算该子表达式.结果评估被存储,直到相应功能的方法接口类型被调用;此时,结果被用作调用的目标引用.这意味着表达式仅当程序在 :: 分隔符之前进行评估遇到方法引用表达式,并且不会重新计算对功能接口类型的后续调用.

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() 时,实例使用存储的引用值.

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.

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

This cannot happen if obj isn't initialized. For example

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

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

The lambda cannot capture the value of obj, because it doesn't have a value yet. It's effectively equivalent to

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 编译器当前对您的代码段的行为方式.

This is how the Oracle compiler is currently behaving for your snippet.

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

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

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 被调用时已完全初始化.

Which is fine because you assume that the Foo instance is completely initialized by the time run is invoked.

相关文章