Java: くらえ! 静的初期化子 (スタティック・イニシャライザ)ーーーッ!!!

久々に Java の文法で「あれ、これどうだっけ?」というのが出てきた。きっと SJC-P (今だと OJC-P?) 受けた人なんかには常識の範囲なのだろうけど。

静的初期化子。static 初期化子とかスタティック・イニシャライザと表記されることもある。以下は、今日時点の自分用まとめ。

基本

静的初期化子は、クラスの初期化に必要な処理を書いたもののことだ。予約語 static の後ろにブロックを書く。例えば static フィールドをある値で初期化したい場合、宣言時に値を指定して

	static int a = 1;

とすることもできるが、静的初期化子を使って

	static int a;
	
	static {
		a = 1;
	}

と書くこともできる。この例のように初期値が単純なリテラルであれば静的初期化子を使うメリットはないが、Map に内容を入れておきたい場合とか、フィールドの初期化以外のこともやりたい場合には、静的初期化子を使うと素直に書くことができる。

静的初期化子は特殊なメソッド (のようなもの) としてコンパイルされ、実行時にはクラスが読み込まれた際に JVM により自動的に呼び出される。JDK 付属の javap コマンドを -c 付きで使うと、コンパイルの結果を確認することができる。上の例を javap -c すると以下のようになる (以下、僕が手元の環境で試した結果を載せるが、コンパイラによっては出力が変わることもあるので、これが絶対というわけではない)。

static int a;

static {};
  Code:
   0:	iconst_1
   1:	putstatic	#10; //Field a:I
   4:	return

なお、宣言時に初期値を指定した場合であっても、本当に静的初期化子なしでいけるのは初期値がプリミティブや文字列で、コンストラクタやメソッドの呼び出しを必要としない場合に限られる。それ以外は、コンパイル時に静的初期化子が自動生成される。

例えば、

	static Integer = Integer.valueOf(1);

と書いて javap -c すると、次のように出力される。

static java.lang.Integer a;

static {};
  Code:
   0:	iconst_1
   1:	invokestatic	#10; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   4:	putstatic	#16; //Field a:Ljava/lang/Integer;
   7:	return

これは、静的初期化子を使って

	static Integer a;
	
	static {
		a = Integer.valueOf(1);
	}

と書いてコンパイルした場合と、まったく同じ結果である。実のところ、JVM には「フィールドの初期化時にメソッドを呼ぶ」という機能がない。つまり、逆の言い方をすると、僕らがフィールドの初期値に好き勝手書けるのは、コンパイラがこのような変換をやってくれるおかげだということだ。

一人っ子じゃなかった

で、ここからが今日の話。

ふと、ひとつのクラスに、静的初期化子を複数回書けることに気付いた。何となく、前にも同じことで驚いたような気がするんだけど、もしかしたら今日初めて知ったのかもしれない。

	static int a;
	
	static {
		a = 1;
	}

	static int b;
	
	static {
		b = 2;
	}

JVM は複数の静的初期化子を扱えないので、ここでもコンパイラが裏で仕事をしてくれる。内容をつなげて、ひとつの静的初期化子に合成してくれるのだ。上の例を javap -c すると次のようになる。

static int a;

static int b;

static {};
  Code:
   0:	iconst_1
   1:	putstatic	#11; //Field a:I
   4:	iconst_2
   5:	putstatic	#13; //Field b:I
   8:	return

これは、次のように書いてコンパイルした場合と同じということだ。

	static int a;
	static int b;
	
	static {
		a = 1;
		b = 2;
	}

複数のフィールドを初期化するような場合には、静的初期化子を分けた方が見やすくなることもあるかもしれない。また、意地の悪い人なら、次のようなコードを Java 初心者に見せて混乱させることもできる。一見、2 つの Map を特別な構文で初期化しているように見えるが、実態はただの静的初期化子、という引っかけである。

	static Map<String, String> names = new HashMap<String, String>(); static {
		names.put("日記", "ダイアリー");
		names.put("しおり", "ブックマーク");
	}

	static Map<String, String> dogs = new HashMap<String, String>(); static {
		dogs.put("しなもん", "犬");
		dogs.put("ウェンディー", "犬");
	}

ただし、静的初期化子が複数書けるのは、このようなわんぱくなコードを書くためではない。初期化子が複数書けないと困る場面があるからだ。それは、初期化時の処理順に関係している。

上から下へ

static フィールドに初期値を指定した場合、初期化は上から順に行われる。例えば次のように書いた場合、a の初期化は必ず b より先に行われる。

	static int a = 1;
	static int b = 2;

ここに静的初期化子を追加した場合、静的初期化子の呼び出しも含めて「上から順に」というルールに従う。

	static int a = 1;
	
	static {
		a = 2;
	}
	
	static int b = 3;
	
	static {
		b = 4;
	}

のように、フィールド宣言の初期値指定と静的初期化子での値代入が交互にあったとしても、上から順に、次のように処理されることが Java 言語仕様できっちり決まっている。

  1. a に 1 が代入される
  2. a に 2 が代入される
  3. b に 3 が代入される
  4. b に 4 が代入される

javap -c してみると、

static int a;

static int b;

static {};
  Code:
   0:	iconst_1
   1:	putstatic	#2; //Field a:I
   4:	iconst_2
   5:	putstatic	#2; //Field a:I
   8:	iconst_3
   9:	putstatic	#3; //Field b:I
   12:	iconst_4
   13:	putstatic	#3; //Field b:I
   16:	return

なんと初期化がすべて静的初期化子にまとめられてしまった (繰り返しになるが、環境によってコンパイル結果がこれとは異なることがあるので注意)。JVM としては「フィールドを初期化した後、静的初期化子を呼ぶ」という 2 段階しかない。一方で Java 言語仕様では「書いたとおり上から順に」ということが決められているため、両方の仕様を満たすために、コンパイラがこのような変換を行ってくれているのだ。わーお。

人生後ろ向き

ところで、Java 言語には前方参照の禁止というルールもある。フィールドの宣言よりも前に書いた部分では、そのフィールドを参照できない、というものだ。したがって、

	static int a = 1;

	static {
		System.out.println(a);
	}

という順で書くならいいけれど、

	static {
		System.out.println(a); // この行でコンパイルエラー
	}
	
	static int a = 1;

と書くとコンパイルエラーになる。a の宣言より前に参照しているためだ。

……で、僕が今日いちばんびっくりしたのは、宣言より前であっても、値を「参照」するのではなく「代入」するのであれば OK らしい、ということだ。

	static {
		a = 0;
		// System.out.println(a);
	}
	
	static int a = 1;

このコードはちゃんとコンパイルできる。javap -c すると

static int a;

static {};
  Code:
   0:	iconst_0
   1:	putstatic	#10; //Field a:I
   4:	iconst_1
   5:	putstatic	#10; //Field a:I
   8:	return

となっていて、ちゃんと「上から順に」処理されている。しかしコメントアウトしてある「// System.out.println(a);」の部分でコメントを外すとやはりコンパイルエラーになる。ちょっと直感に反した動きだ。

また、何でもかんでも無条件に上から処理させているわけではないらしく、フィールドに final 修飾子を付けて次のように書き換えると、やはりコンパイルエラーになる。

	static {
		a = 0; // この行でコンパイルエラー
	}
	
	static final int a = 1;

わかりにくいよ!!

今回は静的初期化子だけ調べたけど、インスタンス初期化子も同じような感じなんだろうなぁ……。

もっとも、上のようなコードは、実際の仕様がどうであれ、ぱっと見ではどうなるかわかりづらい。いずれにしろ書くべきではないコード、ということになるだろう。わかりやすいコードを書いている限りでは問題ないと思われるので、「なんかややこしいみたいだから静的初期化子を使うの禁止な!」というのも極端に過ぎる。

じゃぁどの辺まで OK なのか、ということになるわけだけど、夜も更けてきたのでそろそろ寝ることにする。ぐぅ。