2016-12-07 70 views
3

我遇到了對我來說毫無意義的奇怪行爲。下面的程序(我試過將其降低到最小的例子)與崩潰NullPointerException因爲Bar.YnullJava界面靜態變量未初始化

$ 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第一次被訪問(可以通過取消註釋主方法的第一行或通過重新排序以下兩行來完成)程序正確終止。

我懷疑這個問題與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; 
    } 
} 
+0

@Jobin我已經添加了確切的輸出。 – wotopul

+0

@Jobin它並不重要。我正在使用Intellij IDEA和來自終端的普通Java編譯器。 – wotopul

回答

8

你有Foo接口初始化和FooEnum枚舉初始化之間的循環依賴。通常,FooEnum初始化不會觸發Foo接口初始化,但Foo具有默認方法

The Java® Language Specification, §12.4.1. When Initialization Occurs

當一個類被初始化,其超被初始化(如果它們還沒有被先前初始化),以及任何超級(§8.1.5)即宣佈任何默認的方法( §9.4.3)...

如果你想知道爲什麼默認的方法做改變的行爲,我不知道真正的理由責成這一點。由於實現細節(並且更改規範比更改JVM更容易),看起來更像是在事實because the reference implementation exhibited this behavior之後添加到規範中。


所以,無論何時你有一個循環依賴,結果取決於首先訪問哪種類型。首先被訪問的類型將等待其他類初始化程序的完成,但不會有遞歸。

它可能不是那麼明顯,Foo.A.baz();有這樣的效果,但這種觸發FooEnum含有switchBarEnum聲明初始化。每當一個類包含enumswitch時,它的類初始化程序將爲它準備一個表,因此,在其初始化程序中訪問enum類型的權限,導致其初始化。

這就是爲什麼這會觸發BarEnum初始化,這又會觸發初始化Bar。相比之下,Bar.Y.qux();語句首先直接訪問Bar,觸發其初始化,這又會觸發BarEnum的初始化。

所以你看,執行Foo.A.baz();第一Bar.Y.qux();之前觸發不同的順序Foo.A.baz();之前執行Bar.Y.qux();首先初始化。

如果首先訪問BarEnum,則其類初始化將觸發Bar初始化並推遲其自己的初始化,直到完成Bar初始化程序。換句話說,在這種情況下,當Bar初始值設定項運行時,enum常量字段尚未寫入,所以它將看到null值,並將這些null引用複製到Bar的字段中。

如果首先訪問Bar,則其類初始化將觸發BarEnum初始化,該初始化將寫入枚舉常量,因此在其初始化完成後,將看到正確的初始化值。

+0

是的,我知道這種默認方法怪癖。我在下面糾正了嗎?訪問非常量Foo字段觸發器Foo初始化 - >初始化表達式觸發器FooEnum初始化 - >由於存在缺省方法並且它是一個循環,它將再次觸發Foo初始化。但是,你的意思是什麼?如果它是某種形式不正確的代碼,編譯器不應該丟棄它?如何解釋Bar上NPE的量子效應? AFAIK Java沒有UB。 – wotopul

+1

*「如果它是某種形式不正確的代碼,編譯器不應該丟棄它?」* - 它不是格式錯誤。 *「AFAIK Java沒有UB」* - 你的意思是未定義的行爲?如果是的話,你顯然沒有閱讀JLS 17.特別是JLS 17.4! –

+0

@StephenC你說得對。我的意思是單線程程序的語義:) – wotopul