SJ blog
beginner
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

Javaの初期化順序 — staticブロックからコンストラクタまで何がいつ実行されるか

staticフィールド・staticブロック・インスタンス初期化子・コンストラクタの実行順序と、継承がある場合の順番を根本から解説。「この出力は何か」系の試験問題を確実に解けるようになる。

一言結論

Javaの初期化は「クラスロード時(static)→ インスタンス生成時(フィールド初期化子→コンストラクタ)」の順。継承がある場合は「親static→子static→親コンストラクタ→子コンストラクタ」。この順番を把握すれば出力予測問題は確実に解ける。

初期化には2つのタイミングがある

Javaの初期化処理には2つの異なるタイミングがある:

  1. クラスロード時(プログラム実行中に1回だけ)
  2. インスタンス生成時newするたびに)

この2つを混同すると、試験でよく出る「実行結果を予測せよ」系の問題で詰まる。

クラスロードとは何か

JVMは「あるクラスが初めて必要になったタイミング」でそのクラスをメモリに読み込む。これをクラスロードという。

class Counter {
    static int count = 0;  // クラスロード時に実行
}

// ← この行が実行されると Counter クラスが初めて使われる → クラスロード
Counter c = new Counter();

クラスロードはプログラム全体で1回しか起きない。staticなものはすべてこのタイミングで処理される。

staticの初期化順序

クラスロード時に行われる処理の順番:

class MyClass {
    static int a = 1;          // (1) staticフィールドの初期化
    static int b;

    static {                   // (2) staticブロック(ソースコード順に実行)
        b = a * 2;
        System.out.println("staticブロック: a=" + a + ", b=" + b);
    }

    static int c = b + 10;     // (3) 再びstaticフィールドの初期化(ソースコード順)
}
順番(ソースコードの上から下へ):
  1. a = 1
  2. staticブロック実行 → b = 2, "staticブロック: a=1, b=2" 出力
  3. c = 2 + 10 = 12

重要なのはソースコードの順番通りに実行される点だ。staticフィールドとstaticブロックは「登場した順番」で実行される。

前に書いたstaticフィールドを後のstaticブロックで参照するのはOKだが、後に書いたstaticフィールドを前のstaticブロックで参照するのは未初期化状態を読むことになる:

class Danger {
    static {
        System.out.println(x);  // ← コンパイルエラー: 後方参照(illegal forward reference)
    }
    static int x = 10;
}

インスタンス初期化の順序

newしてインスタンスを作るとき、以下の順番で実行される:

class Person {
    // (1) フィールドのデフォルト初期化(JVMが自動で0/false/null)

    String name = "unknown";      // (2) インスタンスフィールドの初期化
    int age;

    {                             // (3) インスタンス初期化子(フィールドと同じ順番で)
        age = 18;
        System.out.println("インスタンス初期化子: name=" + name);
    }

    String label = name + "_" + age;  // (4) フィールドの続き

    Person() {                    // (5) コンストラクタの本体
        System.out.println("コンストラクタ");
    }

    Person(String name) {
        this.name = name;
        System.out.println("コンストラクタ(name)");
    }
}
new Person() を実行すると:
  1. すべてのフィールドを0/null でデフォルト初期化(JVMが実施)
  2. name = "unknown"
  3. インスタンス初期化子: age = 18, 出力
  4. label = "unknown_18"
  5. コンストラクタ本体: 出力

インスタンス初期化子{}で囲まれた、コンストラクタ外のブロック)はあまり使われないが試験に出やすい。コンストラクタの前に実行されるという点を覚えておく。

コンストラクタとフィールド初期化の関係

class Example {
    int x = 10;          // フィールド初期化子

    Example() {
        System.out.println(x);  // 10(フィールド初期化子がコンストラクタより先)
    }
}

フィールド初期化子はsuper()の後、コンストラクタ本体の前に実行される(コンパイラがそう変換する):

// 実際にコンパイラが生成するコードのイメージ
Example() {
    super();           // 暗黙のsuper()
    this.x = 10;      // フィールド初期化子がここに挿入される
    // コンストラクタ本体
    System.out.println(x);
}

継承がある場合の完全な順序

継承がある場合は以下の順番になる:

class Animal {
    static { System.out.println("1: Animalのstatic"); }

    String type = initType();  // インスタンスフィールド初期化

    { System.out.println("3: Animalのインスタンス初期化子"); }

    Animal() { System.out.println("4: Animalのコンストラクタ"); }

    String initType() { System.out.println("2: Animalのフィールド初期化"); return "animal"; }
}

class Dog extends Animal {
    static { System.out.println("1b: Dogのstatic"); }

    String breed = initBreed();

    { System.out.println("6: Dogのインスタンス初期化子"); }

    Dog() { System.out.println("7: Dogのコンストラクタ"); }

    String initBreed() { System.out.println("5: Dogのフィールド初期化"); return "mix"; }
}

new Dog();

出力:

1: Animalのstatic          ← 親のstaticが先(クラスロード順)
1b: Dogのstatic            ← 子のstatic
2: Animalのフィールド初期化 ← new Dog() → super() → Animalの初期化開始
3: Animalのインスタンス初期化子
4: Animalのコンストラクタ
5: Dogのフィールド初期化   ← Animalの初期化が終わってからDogの初期化
6: Dogのインスタンス初期化子
7: Dogのコンストラクタ

ルールをまとめると:

クラスロード時(1回だけ):
  親のstaticフィールド/staticブロック(ソースコード順)
  子のstaticフィールド/staticブロック(ソースコード順)

new のたびに:
  親のフィールド初期化子 + インスタンス初期化子(ソースコード順)
  親のコンストラクタ本体
  子のフィールド初期化子 + インスタンス初期化子(ソースコード順)
  子のコンストラクタ本体

試験頻出パターン:staticとインスタンスの混同

class Counter {
    static int staticCount = 0;
    int instanceCount = 0;

    Counter() {
        staticCount++;
        instanceCount++;
    }
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.staticCount);  // 3(全インスタンスで共有)
System.out.println(c1.instanceCount);     // 1(c1だけがインクリメントした分)
System.out.println(c2.instanceCount);     // 1
System.out.println(c3.instanceCount);     // 1

staticフィールドは全インスタンスで共有されるのでnewするたびに増え続ける。インスタンスフィールドは各インスタンス固有なので毎回1から始まる。

前方参照の落とし穴

class Forward {
    int x = y + 1;  // ❌ コンパイルエラー(yはまだ宣言されていない)
    int y = 10;
}

インスタンスフィールドも宣言より前のフィールドを参照するとコンパイルエラーになる。ソースコード順に初期化されるので、「まだ初期化されていないフィールドを参照しようとした」と検出される。

ただし、メソッド越しなら実行時まで評価が遅延されるのでコンパイルエラーにはならない(が、実行結果が想定外になることがある):

class Tricky {
    int x = getY() + 1;  // ← メソッド呼び出し。コンパイルは通る
    int y = 10;

    int getY() { return y; }  // yはまだ0(デフォルト値)
}

new Tricky().x;  // 0 + 1 = 1(yが10になる前にgetY()が呼ばれたので)

まとめ

static(クラスロード時、1回だけ):
  ソースコード順に staticフィールド初期化子 と staticブロック を実行

インスタンス(newのたびに):
  1. デフォルト初期化(JVMが 0/false/null に)
  2. super() 実行(親の初期化)
  3. フィールド初期化子 と インスタンス初期化子(ソースコード順)
  4. コンストラクタ本体

継承がある場合の順番:
  親static → 子static → 親のフィールド+初期化子 → 親コンストラクタ → 子のフィールド+初期化子 → 子コンストラクタ

「この出力は何か」型の試験問題で詰まるのはほぼ全部この順番が原因だ。staticとインスタンスの2つのタイミングを分けて、それぞれでソースコード順に実行されると覚えておけば、複雑に見える問題も一つずつ追えるようになる。