4

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;
    }
}
wotopul
  • 115
  • 1
  • 2
  • 10

1 Answers1

10

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.

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Yes, I'm aware of this default methods quirk. Am I correct in the following? Access of non-constant Foo field triggers Foo initialization -> initializing expression triggers FooEnum initialization -> which triggers Foo initialization again because of the presence of a default method and it's a cycle. But what exactly your point is? If it is some kind of malformed code shouldn't the compiler throw it away? And how to explain quantum effects with NPE on Bar? AFAIK Java does not have UB. – wotopul Dec 07 '16 at 12:46
  • 1
    *"If it is some kind of malformed code shouldn't the compiler throw it away?"* - It isn't malformed. *"AFAIK Java does not have UB"* - Do you mean undefined behavior? If yes, you obviously haven't read JLS 17. Especially JLS 17.4! – Stephen C Dec 07 '16 at 12:57
  • @StephenC You're right. I mean semantics of the single-threaded programs :) – wotopul Dec 07 '16 at 13:01
  • 2
    This isn’t UB as the behavior is precisely specified and it can be retraced what happens. I have expanded my answer to describe that. All conforming JVMs will do the same. – Holger Dec 07 '16 at 13:02
  • 1
    @Stephen C: this still isn’t UB in the `C`/`C++` sense, where *anything* could happen. The memory model still defines a finite set of what could happen, even if that set could grow quite large in a complex application with a data race. – Holger Dec 07 '16 at 13:04
  • 1
    @Holger - You are right. I was responding to the comment. However, if we are nit-picking, true UB (in the C/C++) sense is possible if you use native code, `Unsafe`, bytecode engineering, certain reflective operations on `final` fields and possibly other things. – Stephen C Dec 07 '16 at 13:33
  • 1
    @Stephen C: reflective operations are well specified, even when modifying `final` fields this way. Everything else is outside the Java scope. Otherwise, we should mention hardware hacking as well… – Holger Dec 07 '16 at 13:38
  • Sorry for bumping this up again but it appears that either I didn't understand you correctly or that things are actually more complicated than it is described in your answer. First, I examined the order of class initialization by putting simple print statements (see updated question). And interfaces get initialized prior corresponding enums in either case. (I don't know though if this way to view initialization order is correct in case of circular dependencies). – wotopul Dec 08 '16 at 16:18
  • And second, there is no NPE if you remove switch statement from `FooEnum#baz()` which has nothing to do with `BarEnum`. Could you clear it up? – wotopul Dec 08 '16 at 16:19
  • The interface `Bar` only triggers the initialization of `BarEnum`, when it actually uses that class. You have placed the `int barF = InitUtil.initInt("[Bar]");` declaration *before* the fields that use `BarEnum`, so it will always print `[Bar]` before that. That’s different to `BarEnum` which *implements* `Bar` and hence will trigger the initialization of that dependency even before its own initializer started. – Holger Dec 08 '16 at 16:39
  • The way, `switch` over `enum` is implemented, is compiler dependent. I had to disassemble to understand what’s going on with `javac`. It places all switch tables into another anonymous class, so the first use of a `switch` over any `enum` will trigger the initialization of that anonymous class, hence, *all* switch tables of that class. In contrast, Eclipse uses distinct lazy initialization for every enum type, so the code doesn’t throw a `NullPointerException` at all. However, I’m wondering whether Eclipse’s strategy can cause multi-threading bugs… – Holger Dec 08 '16 at 16:59
  • Thanks again. And pardon my silliness with print statements (I see now why this approach obviously does not work). – wotopul Dec 08 '16 at 17:07