Java接口静态变量未初始化

时间:2016-12-07 12:02:48

标签: java java-8 initialization

我遇到了对我没有意义的奇怪行为。以下程序(我已尝试将其缩减为最小示例)与Bar.Y崩溃,因为null$ javac *.java $ java Main FooEnum.baz() Exception in thread "main" java.lang.NullPointerException at Main.main(Main.java:6)

FooEnum.baz()
Bar.qux

我希望它能打印出来:

Bar.qux

但是,如果首先访问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; } } (可以通过取消注释main方法的第一行或重新排序以下两行来完成),程序将正确终止。

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

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

我的JDK版本是1.8.0_111

<?php get_header(); ?>
<?php if( have_posts() ) : the_post(); ?>
    <?php the_content(); ?>
<?php endif; ?>
<?php get_footer(); ?>

1 个答案:

答案 0 :(得分:8)

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

请参阅The Java® Language Specification, §12.4.1. When Initialization Occurs

  

当一个类被初始化时,它的超类被初始化(如果它们之前没有被初始化),以及声明任何默认方法的任何超接口(第8.1.5节)(§9.4.3)...

如果你想知道为什么默认方法会改变行为,我不知道要求这个的真正原理。由于实现细节(并且更改规范比更改JVM更容易),事实上because the reference implementation exhibited this behavior之后似乎更多地将其添加到规范中。

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

Foo.A.baz();可能不那么明显,但这会触发FooEnum的初始化,其中包含switch超过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值,并复制这些nullBar

字段的引用

如果首先访问Bar,其类初始化将触发BarEnum初始化,该初始化将写入枚举常量,因此在完成后,Bar初始化程序将看到正确初始化的值。