我正在经历对我来说没有意义的奇怪行为。以下程序(我已尝试将其简化为最少的示例)崩溃,NullPointerException
原因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
首先访问(可以通过取消注释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;
}
}
Foo
接口初始化和FooEnum
枚举初始化之间具有循环依赖关系。通常,FooEnum
初始化不会触发Foo
接口初始化,但是Foo
具有默认方法。
初始化一个类时,将初始化其超类(如果之前尚未对其进行初始化)以及声明任何默认方法的任何超接口(第8.1.5节)(第9.4.3节)……
如果您想知道为什么默认方法会更改行为,我不知道强制执行此操作的真正理由。实际上,这似乎是在事实之后添加到规范中的,因为参考实现由于实现细节而表现出了这种行为(更改规范比更改JVM更容易)。
因此,只要您具有循环依赖关系,结果就取决于首先访问哪种类型。首先访问的类型将等待其他类初始化程序的完成,但不会进行递归。
可能没有那么明显Foo.A.baz();
的作用,但这会触发FooEnum
包含switch
overBarEnum
语句的初始化。每当一个类包含一个时enum
switch
,它的类初始化器都会为其准备一个表,从而enum
在其初始化器中直接访问该类型,从而导致其初始化。
这就是为什么这会触发BarEnum
初始化,进而触发Bar
初始化。相反,该Bar.Y.qux();
语句Bar
首先直接访问,触发其初始化,进而触发的初始化BarEnum
。
因此,您可以看到,执行Foo.A.baz();
first beforeBar.Y.qux();
会以不同于执行Bar.Y.qux();
first before的顺序触发初始化Foo.A.baz();
。
如果BarEnum
首先访问,则其类初始化将触发Bar
初始化,并将其自身的初始化推迟到初始化程序完成之前Bar
。换句话说,在这种情况下,初始化程序运行enum
时尚未写入常量字段Bar
,因此它将看到null
它们的值并将这些null
引用复制到的字段中Bar
。
如果Bar
首先访问它,则其类初始化将触发BarEnum
初始化,该初始化将写入枚举常量,因此完成后,Bar
初始化器将看到正确的初始化值。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句