我遇到了对我没有意义的奇怪行为。以下程序(我已尝试将其缩减为最小示例)与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(); ?>
答案 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
值,并复制这些null
对Bar
。
如果首先访问Bar
,其类初始化将触发BarEnum
初始化,该初始化将写入枚举常量,因此在完成后,Bar
初始化程序将看到正确初始化的值。