【问题标题】:Java "The blank final field may not have been initialized" Anonymous Interface vs Lambda ExpressionJava“可能尚未初始化空白的最终字段”匿名接口与 Lambda 表达式
【发布时间】:2015-05-20 21:35:55
【问题描述】:

我最近遇到了错误消息“空白的最终字段 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 表达式,它不会“用长版本替换它”。但是,为什么这会影响我在创建Runnable 对象时不运行run() 方法中的代码部分?我仍然能够在调用run() 之前进行初始化。所以从技术上讲,这里可能不会遇到NullPointerException。 (虽然最好也在这里检查null。但这个约定是另一个话题。)

我犯了什么错误? lambda 的处理方式有何不同,以至于它会影响我的对象使用方式?

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

【问题讨论】:

  • 最终版本有什么警告/错误?您使用的是什么版本的 Java?
  • @SotiriosDelimanolis 我得到了标题中提到的确切错误。而且我使用 Java 8。我想否则我什至无法使用 lambda,对吧?
  • 对不起,我指的是编译器版本。它是 java 8,但是是哪个发行版本。

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


【解决方案1】:

你可以绕过这个问题

        Runnable run = () -> {
            (this).obj.toString(); 
        };

这在 lambda 开发过程中讨论过,基本上 lambda 主体在明确分配分析过程中被视为本地代码

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

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

坦率地说,我和其他一些人认为这个决定是错误的。 lambda 只捕获this,而不是obj。这种情况应该被视为与匿名类相同。当前行为对于许多合法用例来说都是有问题的。好吧,你总是可以使用上面的技巧绕过它——幸运的是 明确的分配分析不太聪明,我们可以愚弄它。

【讨论】:

  • 我不认为编译的技术方面应该驱动规范的正式定义。从技术上讲,今天编译的 lambdas 捕获 this 以访问字段,甚至是 final 字段,但这可能会在未来的版本中改变。并且流分析不会阻止Runnablerun 方法在obj 初始化之前被调用。并且 lambdas 应该 not 表现得像内部类,因为它们不是内部类。他们在很多方面都不像他们,如果让他们在这方面表现得像他们,那就是倒退了。
  • capturing-this-only 在规范中,我认为他们不能在未来的版本中改变它。
  • 您能否提供规范相关部分的链接?我在规范中没有找到任何关于这种行为的信息。
  • @Holger - 伙计,我应该 - 但我真的不想再深入规范;这是一头野兽。
  • 咳咳,这个问题是关于什么的?事实上,对于由简单名称或带有this(不带大括号)限定的简单名称组成的表达式,编译器确实执行此规则。因此,对于这些类型的访问,不需要通过重复读取来访问该字段。其余的由Memory Model 给出:“……编译器可以将最终字段的值保存在寄存器中,并且在必须重新加载非最终字段的情况下,不会从内存中重新加载它”
【解决方案2】:

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

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

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

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

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

类似的事情发生在

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 编译器当前对您的 sn-p 的行为方式。

然而,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 时已完全初始化。

【讨论】:

  • @SteffenT 版本:Luna 版本 (4.4.0) 内部版本 ID:20140612-0600
  • @SteffenT 我试图找到一个解释捕获逻辑的 JLS 部分,但我找不到它。我试着这样看:解析 lambda 表达式的主体,并提取所有未在主体中声明的变量。然后评估变量的当前值并将其绑定到它们。它们在生成的实例的生命周期内不会改变。
  • @SteffenT 我不明白。该变量将存储一些常量值。如果该值是引用类型,您可以取消引用它以调用方法或访问字段。
  • @SteffenT 查看我的上一个代码 sn-p。这相当于我认为编译器应该为 your last sn-p 做的事情。 lambda 会将Foo.this 的值存储在privatefinal 字段中。也就是说,它是不可变的/恒定的。不过,您仍然可以访问其字段 obj。或者调用它的任何方法。
  • 只捕获this,从不捕获this 的实例变量。正如规范所规定的,这也是 Oracle javac 的行为。问题源于其他地方。不幸的是,根据规范,eclipse 在这里是错误的。
【解决方案3】:

您可以使用实用程序方法仅强制捕获this。这也适用于 Java 9。

public static <T> T r(T object) {
    return object;
}

现在,您可以像这样重写您的 lambda:

Runnable run = () -> r(this).obj.toString();

【讨论】:

    【解决方案4】:

    我也遇到过类似的问题:

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

    【讨论】:

      猜你喜欢
      • 2017-06-05
      • 2017-12-01
      • 2016-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多