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

时间:2015-05-20 21:35:55

标签: java constructor lambda compiler-construction java-8

我最近遇到了错误消息“空白的最终字段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的处理方式如此不同,它会影响我的对象使用方式吗?

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

4 个答案:

答案 0 :(得分:15)

您可以通过

绕过问题
        Runnable run = () -> {
            (this).obj.toString(); 
        };

在lambda开发过程中对此进行了讨论,基本上在明确赋值分析期间将lambda主体视为本地代码。

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

  

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

中使用没有例外
坦率地说,我和其他一些人认为这个决定是错误的。 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的值,而是捕获thisFoo实例)的值。它实际上等同于

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.objObjectHolder.this.obj都不在Eclipse中工作(尽管后者在标准JDK编译器中工作)。

对于您而言,使用此替代方法,对于所有编译器都是安全的:

((Foo)this).obj.toString();

另一种解决方案是使用吸气剂。在我的示例中,它看起来像这样:

public Supplier<Object> sup = () -> getObj();

private Object getObj() {
    return obj;
}