Java接口静态变量未初始化
我正在经历对我来说毫无意义的奇怪行为。以下程序(我已尝试将其简化为最小示例)因Java接口静态变量未初始化,java,java-8,initialization,Java,Java 8,Initialization,我正在经历对我来说毫无意义的奇怪行为。以下程序(我已尝试将其简化为最小示例)因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 但是,如果首先访问Ba
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
接口初始化和fooneum
enum初始化之间存在循环依赖关系。通常,FooEnum
初始化不会触发Foo
接口初始化,但Foo
具有默认方法
见:
当一个类被初始化时,它的超类被初始化(如果它们之前没有被初始化),以及声明任何默认方法的任何超接口(§8.1.5)(§9.4.3)
如果您想知道为什么默认方法会改变行为,我不知道强制这样做的真正理由。由于实现细节的原因,这更像是事后添加到规范中的(更改规范比更改JVM更容易)
因此,每当您有一个循环依赖项时,结果取决于首先访问哪个类型。首先访问的类型将等待另一个类初始值设定项的完成,但不会有递归 这可能不太明显,
Foo.A.baz()
具有这样的效果,但这会触发FooEnum
的初始化,其中包含switch
overBarEnum
语句。每当类包含枚举
开关
时,它的类初始值设定项都会为它准备一个表,因此,访问其初始值设定项中的枚举
类型,导致初始化
这就是为什么这会触发BarEnum
初始化,而这反过来又会触发Bar
初始化。相反,Bar.Y.qux()
语句首先直接访问条
,触发其初始化,然后触发BarEnum
的初始化
你看,执行Foo.A.baz()代码>第一个在Bar.Y.qux()之前
以与执行Bar.Y.qux()不同的顺序触发初始化代码>前一个Foo.A.baz()代码>
如果首先访问BarEnum
,则其类初始化将触发Bar
初始化,并将其自身的初始化延迟到Bar
初始化器完成。换句话说,在这种情况下,当Bar
初始值设定项运行时,enum
常量字段尚未写入,因此它将看到它们的null
值,并将这些null
引用复制到Bar
的字段
如果首先访问Bar
,其类初始化将触发BarEnum
初始化,该初始化将写入枚举常量,因此完成后,Bar
初始化器将看到正确的初始化值。@Jobin我已添加了确切的输出。@Jobin这并不重要。我正在使用Intellij IDEA和terminal的普通Java编译器。“如果它是某种格式错误的代码,编译器不应该扔掉它吗?”-它不是格式错误。“AFAIK Java没有UB”-您是指未定义的行为吗?如果是,您显然没有阅读JLS 17。特别是JLS 17.4!这不是UB,因为行为是精确指定的,可以追溯发生了什么。我已经扩展了我的答案来描述这一点。所有符合条件的JVM都会这样做。@Stephen C:这仍然不是UB在C
/C++
的意义上,任何事情都可能发生。内存模型仍然定义了一个有限的可能发生的事情集,即使这个集合在一个数据竞争的复杂应用程序中会变得相当大。@Holger-你说得对。我是在回应这个评论。但是,如果我们吹毛求疵,如果您使用本机代码、不安全的
、字节码工程、对final
字段的某些反射操作以及可能的其他内容,那么真正的UB(在C/C++)意义上是可能的。@Stephen C:反射操作是非常明确的,即使以这种方式修改final
字段。其他一切都在Java范围之外。否则,我们也应该提到硬件黑客…