Java接口静态变量未初始化

wot*_*pul 3 java initialization java-8

我正在经历一种对我来说没有意义的奇怪行为.以下程序(我试图将其减少到最小的例子)崩溃NullPointerException因为Bar.Ynull:

$ javac *.java
$ java Main
FooEnum.baz()
Exception in thread "main" java.lang.NullPointerException
    at Main.main(Main.java:6)
Run Code Online (Sandbox Code Playgroud)

我希望它打印:

FooEnum.baz()
Bar.qux
Run Code Online (Sandbox Code Playgroud)

但是,如果Bar.qux首先访问(可以通过取消注释main方法的第一行或通过重新排序以下两行来完成),程序将正确终止.

我怀疑这个问题与Java类初始化顺序有关,但我无法在相关的JLS部分找到任何解释.

所以,我的问题是:这里发生了什么?这是某种错误还是我错过了什么?

我的JDK版本是1.8.0_111

interface Bar {
    // UPD
    int barF = InitUtil.initInt("[Bar]");

    Bar X = BarEnum.EX;
    Bar Y = BarEnum.EY;

    default void qux() {
        System.out.println("Bar.qux");
    }
}

enum BarEnum implements Bar {
    EX,
    EY;

    // UPD
    int barEnumF = InitUtil.initInt("[BarEnum]");
}

interface Foo {
    Foo A = FooEnum.EA;
    Foo B = FooEnum.EB;

    // UPD
    int fooF = InitUtil.initInt("[Foo]");

    double baz();

    double baz(Bar result);
}

enum FooEnum implements Foo {
    EA,
    EB;

    // UPD
    int fooEnumF = InitUtil.initInt("[FooEnum]");

    public double baz() {
        System.out.println("FooEnum.baz()");
        // UPD this switch can be replaced with `return 42`
        switch (this) {
            case EA: return 42;
            default: return 42;
        }
    }

    public double baz(Bar result) {
        switch ((BarEnum) result) {
            case EX: return baz();
            default: return 42;
        }
    }

}

public class Main {
    public static void main(String[] args) {
        // Bar.Y.qux(); // uncomment this line to fix NPE
        Foo.A.baz();
        Bar.Y.qux();
    }
}

// UPD
public class InitUtil {
    public static int initInt(String className) {
        System.out.println(className);
        return 42;
    }
}
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 8

您在Foo接口初始化和FooEnum枚举初始化之间存在循环依赖关系.通常,FooEnum初始化不会触发Foo接口初始化,但Foo具有默认方法.

请参阅Java®语言规范,§12.4.1.发生初始化时:

初始化类时,初始化其超类(如果它们之前未初始化),以及声明任何默认方法的任何超接口(第8.1.5节)(第9.4.3节)......

如果你想知道为什么默认方法会改变行为,我不知道要求这个的真正原理.看起来更像是在事实之后将其添加到规范中,因为参考实现由于实现细节而表现出这种行为(并且更改规范比更改JVM更容易).


因此,只要有循环依赖关系,结果就取决于首先访问的类型.首先访问的类型将等待另一个类初始值设定项的完成,但不会有递归.

Foo.A.baz();具有这样的效果可能不那么明显,但是这会触发初始化,FooEnum其中包含switchover BarEnum语句.每当一个类包含一个类时enum switch,它的类初始化器将为它准备一个表,因此,enum在其初始化器中访问该类型,导致其初始化.

这就是为什么这会触发BarEnum初始化,从而触发Bar初始化.相反,Bar.Y.qux();语句Bar首先直接访问,触发其初始化,从而触发初始化BarEnum.

所以你看,Foo.A.baz();先执行先Bar.Y.qux();触发初始化的顺序与Bar.Y.qux();之前执行的顺序不同Foo.A.baz();.

如果BarEnum首先访问它,它的类初始化将触发Bar初始化并推迟其自己的初始化,直到初始化程序完成Bar.换句话说,在这种情况下,初始化程序运行enum时尚未写入常量字段Bar,因此它将看到null它们的值并将这些null引用复制到字段中Bar.

如果Bar首先访问它,它的类初始化将触发BarEnum初始化,这将写入枚举常量,因此在完成时,Bar初始化器将看到正确初始化的值.

  • 这不是UB,因为行为是精确指定的,它可以回溯发生的事情.我已经扩展了我的答案来描述这一点.所有符合标准的JVM都会这样做. (2认同)