2016-12-07 23 views
3

私には意味をなさない奇妙な動作が発生しています。私は印刷にそれを期待Javaインターフェイスの静的変数が初期化されていません

$ javac *.java 
$ java Main 
FooEnum.baz() 
Exception in thread "main" java.lang.NullPointerException 
    at Main.main(Main.java:6) 

NullPointerExceptionnullあるBar.Yため、次のプログラムがでクラッシュ(私は、最小限の例にそれを軽減しようとした)Bar.quxに最初にアクセスした場合は

FooEnum.baz() 
Bar.qux 

(メインメソッドの最初の行をコメント解除するか、または次の2行を並べ替えることによって実行できます)、プログラムは正しく終了します。

この問題は、Javaクラスの初期化の順序と関係していると思われますが、関連するJLSセクションで説明が見つかりませんでした。

私の質問は、ここで何が起こっているのですか?これは何らかのバグですか、何か不足していますか?

私のJDKのバージョンあなたはFooインターフェイスの初期化とFooEnum列挙型の初期化の間に循環依存を持つ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

@ジョビンそれは本当に問題ではありません。私はIntellij IDEAとターミナルからプレーンJavaコンパイラの両方を使用しています。 – wotopul

答えて

8

です。通常、FooEnum初期化はFooインターフェイスの初期化をトリガーしませんが、Fooデフォルトメソッドです。任意のデフォルトのメソッドを(宣言している(§8.1.5)クラスが初期化されると(彼らは以前に初期化されていない場合)、そのスーパークラスが初期化されている

、ならびに任意のスーパーインタフェース:

The Java® Language Specification, §12.4.1. When Initialization Occursを参照してください。デフォルトのメソッドは、動作を変更しない理由を知りたい場合は§9.4.3)...

、私はこれを強制するために、実際の根拠を知りません。これは、実装の詳細のために、because the reference implementation exhibited this behaviorという事実の後で仕様に追加された(そして、仕様の変更はJVMを変更するよりも簡単だったようです)。


循環依存性がある場合は常に、最初にどの型にアクセスするかによって結果が異なります。最初にアクセスされた型は、他のクラス初期化子の完了を待機しますが、再帰はありません。

Foo.A.baz();は、このような効果を持っているが、これはswitchBarEnum以上の文を含むFooEnumの初期化をトリガーすることそれはとても明白でない場合があります

。クラスにenumswitchが含まれている場合は、そのクラスの初期化子がテーブルを準備します。したがって、enumタイプにアクセスして初期化します。

これがBarEnumの初期化をトリガーし、Barの初期化をトリガーする理由です。対照的に、Bar.Y.qux();ステートメントはBarに直接アクセスし、その初期化をトリガーし、次にBarEnumの初期化をトリガーします。

だからBar.Y.qux();Foo.A.baz();前に最初Bar.Y.qux();を実行するとは異なる順序で初期化をトリガーする前に、最初のFoo.A.baz();を実行し、参照してください。

BarEnumに最初にアクセスした場合、そのクラスの初期化Bar初期化をトリガし、Bar初期化が完了するまで、自身の初期化を延期します。つまり、この場合、Barイニシャライザの実行時にenumの定数フィールドが書き込まれていないため、nullの値が表示され、nullの参照がBarのフィールドにコピーされます。 Barに最初にアクセスした場合

、そのクラスの初期化は、その完了時に、Barイニシャライザが正しく初期化された値が表示されるように、enum定数を書き込むBarEnum初期化をトリガします。

+0

はい、私はこのデフォルトの方法が奇抜であることに気付いています。私は次の点で正しいですか?非定数FooフィールドのアクセスFooの初期化 - >初期化の式のトリガーFooEnumの初期化 - > Fooの初期化がデフォルトメソッドの存在のために再びトリガーされ、サイクルです。しかし、正確にあなたのポイントは何ですか?それが何らかの種類の不正なコードであれば、コンパイラはそれを投げ捨ててはいけませんか?また、NPEでバーに量子効果を説明する方法は? AFAIK JavaにはUBがありません。 – wotopul

+1

* "何らかの種類の不正なコードであれば、コンパイラはそれを投げ捨ててはいけませんか?" * - 不正な形式ではありません。 * "AFAIK JavaにはUBがありません" * - 未定義の動作を意味しますか?はいの場合は、明らかにJLS 17を読んでいません。特にJLS 17.4! –

+0

@StephenCあなたはそうです。私はシングルスレッドプログラムのセマンティクスを意味します:) – wotopul

関連する問題