I'm experiencing strange behavior that does not make sense to me. The following program (I've tried to reduce it to minimal example) crashes with NullPointerException
because Bar.Y
is null
:
$ javac *.java
$ java Main
FooEnum.baz()
Exception in thread "main" java.lang.NullPointerException
at Main.main(Main.java:6)
I expect it to print:
FooEnum.baz()
Bar.qux
However if Bar.qux
is accessed first (it could be done by either uncommenting the first line of the main method or by reordering the following two lines) the program terminates correctly.
I suspect this issue has something to do with Java class initialization order but I was unable to find any explanation in relevant JLS sections.
So, my question is: what is going on here? Is this some kind of bug or am I missing something?
My JDK version is 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;
}
}
You have a circular dependency between the Foo
interface initialization and FooEnum
enum initialization. Normally, the FooEnum
initialization wouldn’t trigger the Foo
interface initialization, but Foo
has default methods.
See The Java® Language Specification, §12.4.1. When Initialization Occurs:
When a class is initialized, its superclasses are initialized (if they have not been previously initialized), as well as any superinterfaces (§8.1.5) that declare any default methods (§9.4.3)…
If you want to know why default methods do change the behavior, I don’t know a real rationale to mandate this. It seems more like that this was added to the specification after the fact, because the reference implementation exhibited this behavior due to implementation details (and changing the specification was easier than changing the JVM).
So whenever you have a circular dependency, the result depends on which type is accessed first. The type which has been accessed first will wait for the completion of the other class initializer, but there will be no recursion.
It might not so obvious that Foo.A.baz();
has such an effect, but this triggers the initialization of FooEnum
which contains a switch
over BarEnum
statement. Whenever a class contains an enum
switch
, it’s class initializer will prepare a table for it, thus, access the enum
type right in its initializer, causing its initialization.
That’s why this triggers the BarEnum
initialization, which in turn triggers the Bar
initialization. In contrast, the Bar.Y.qux();
statement directly accesses Bar
first, triggering its initialization, which in turn triggers the initialization of BarEnum
.
So you see, executing Foo.A.baz();
first before Bar.Y.qux();
triggers the initialization in a different order than executing Bar.Y.qux();
first before Foo.A.baz();
.
If BarEnum
is accessed first, its class initialization will trigger the Bar
initialization and defer its own initialization until the completion of the Bar
initializer. In other words, in this case, the enum
constant fields have not been written when the Bar
initializer runs, so it will see null
values for them and copy these null
references to the fields of Bar
.
If Bar
is accessed first, its class initialization will trigger the BarEnum
initialization which will write the enum constants, so upon its completion, the Bar
initializer will see correctly initialized values.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With