読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

「Classオブジェクトを生成済み→staticイニシャライザを実行済み」ではない

まえがき


今日はJavaマニアックコースです。フツーのプログラムを書いている場合、こういう事は一切気にしなくて良いです。

というか、気にしないといけないようなプログラムを書いてはイカン。


今日のお題

さて、先程、こんなコードを書いたのです。

public enum Foo {
    ABC;

    static {
        System.out.println("Foo.static");
    }
}

public class Main {
    public static void main(String... args) throws Exception {
        System.out.println(Foo.class);
    }
}


僕は、この実行結果は

Foo.static
class Foo

になると思っていたのです。ついうっかりと。
しかし実際は

class Foo


になってしまいます。つまりstaticイニシャライザは実行されていない。
クラスリテラルを使っているだけでは、クラス初期化は行われない模様です。


Class#toString()は実行している(つまり、Fooのクラスオブジェクトへはアクセス出来る)のに、クラスは初期化されていないというわけです。
そんな中途半端な状態があり得るのか…

JLSには何と書いてあるんだろうか。あとで読んでみよう。

でもって、

public enum Foo {
    ABC;

    static {
        System.out.println("Foo.static");
    }
}

public class Main {
    public static void main(String... args) throws Exception {
        System.out.println(Class.forName("Foo"));
    }
}


の結果は

Foo.static
class Foo


になります。まあ当たり前ですが。

そして、

この実行結果は

public enum Foo {
    ABC;

    static {
        System.out.println("Foo.static");
    }
}

public class Main {
    public static void main(String... args) throws Exception {
        System.out.println(Foo.ABC);
    }
}


これになります。

Foo.static
ABC


スタティックフィールドにアクセスした時点でクラス初期化が走ります。(JLSに書いてある通り)
ですのでコレも当たり前の動作です。

では、

リフレクションを使うとどうなるのか?を調べてみたところ、

public enum Foo {
    ABC;

    static {
        System.out.println("Foo.static");
    }
}

public class Main {
    public static void main(String... args) throws Exception {
        Field field = Foo.class.getField("ABC");
        System.out.println(field);
        System.out.println(field.get(null));
    }
}


実行結果は次の通りとなりました。

public static final Foo Foo#ABC
Foo.static
ABC


要するに、ひとつ上の例と同等の結果になります。
初めてスタティックフィールドにアクセスしに行ったときにクラス初期化が行われるというわけです。


うーむ、なかなかに奥が深いですな。
まあ、JLSちゃんと読まんかいという感じです。

はいはい読みゃーいいんでしょっと。

JLS先生!!


JLSでstaticイニシャライザに該当する部分はココです。
http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.7

Any static initializers declared in a class are executed when the class is initialized and, together with any field initializers (§8.3.2) for class variables, may be used to initialize the class variables of the class (§12.4).

staticイニシャライザはクラス変数を初期化するためのものである。
クラスが初期化されたときに、クラス変数のフィールドイニシャライザと同時に実行される。


    StaticInitializer:
        static Block


It is a compile-time error for a static initializer to be able to complete abruptly (§14.1, §15.6) with a checked exception (§11.2). It is a compile-time error if a static initializer cannot complete normally (§14.21).

staticイニシャライザがチェック例外をスローする場合や、ブロックが正常に終了していない場合は
コンパイルエラーになる。


The static initializers and class variable initializers are executed in textual order.

staticイニシャライザとクラス変数イニシャライザはコードに書いてある順序で実行される。


Use of class variables whose declarations appear textually after the use is sometimes restricted, even though these class variables are in scope. See §8.3.2.3 for the precise rules governing forward reference to class variables.

使う箇所よりも後で宣言されているクラス変数を使おうとするとき、いくつかの制約がつく。
スコープ内にあるかどうかは関係ない。
クラス変数の先読み参照についてのルールは8.3.2.3を参照せよ。


If a return statement (§14.17) appears anywhere within a static initializer, then a compile-time error occurs.

staticイニシャライザ内でreturn文を使うとコンパイルエラーになる。



If the keyword this (§15.8.3) or any type variable (§4.4) defined outside the initializer or the keyword super (§15.11, §15.12) appears anywhere within a static initializer, then a compile-time error occurs.

this、superキーワードやstaticイニシャライザの外で宣言されている型変数を使おうとするとコンパイルエラーになる。


...うーむ、「そんなこと知っとるわい」という情報しか載ってない。


クラスリテラルについて解説している章は次の通り。
http://java.sun.com/docs/books/jls/second_edition/html/expressions.doc.html#15.8.2

A class literal is an expression consisting of the name of a class, interface, array, or primitive type followed by a `.' and the token class. The type of a class literal is Class. It evaluates to the Class object for the named type (or for void) as defined by the defining class loader of the class of the current instance.


クラスリテラルとは、『クラス/インタフェース/配列/プリミティブ型』の型名の後ろに'.class'を付けた式である。
クラスリテラルはClass型の値であり、指定した名前のClassオブジェクトとして評価される。
クラスの探索には、現在のインスタンス(*)のクラスをロードしたクラスローダーが用いられる。


(*) 訳注: クラスリテラルを使っているコードを実行しているインスタンスのこと。


...うおぃ!こっちの情報も役に立たねぇじゃないですか。

答えはココに

http://java.sun.com/docs/books/jls/third_edition/html/execution.html#44557

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

クラスまたはインタフェース(仮にTとする)は以下のどれかが発生しようとする直前で、直ちに初期化される。

  • T is a class and an instance of T is created.
  • T is a class and a static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).
  • T is a top-level class, and an assert statement (§14.10) lexically nested within T is executed.
  • Tのインスタンスが作成される。
  • Tで宣言されているスタティックメソッドが実行される。
  • Tで宣言されているスタティックフィールドに代入が行われる。
  • Tで宣言されているスタティックフィールド(ただし定数以外)にアクセスされる。(*)
  • Tがトップレベルクラスのとき、Tの内部にあるassert文が実行される。


(*) 訳注:ここで言う『定数』とは、コンパイル時にインライン展開可能(一種のコンパイル時最適化)なプリミティブ型や文字列のことです。インライン展開されているから、コード実行時には実際のフィールドアクセスは発生し得ないわけです。詳しくはJLSの4.12.4を参照。


はい、どこにも「クラスオブジェクトにアクセスされた」とは書いてないですね。


だから、クラスオブジェクト(含むクラスリテラル)を操作している程度では、クラスの初期化は実行されないというわけです。


…まあ少なくともSunJVMでは。他の処理系は知りませぬ。