Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java interface static variable is not initialized

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;
    }
}
like image 622
wotopul Avatar asked Feb 06 '23 14:02

wotopul


1 Answers

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.

like image 85
Holger Avatar answered Feb 08 '23 05:02

Holger