定数フィールドへの参照はコンパイル時にバイナリに展開される

最近知ったこと。
いや、『Java魂』(O'REILLY)を読んだから知ってるはずだけど忘れていた事実。



public static final String HOGE = "hoge";
のような定数への参照は、コンパイル時にバイナリファイルにインライン展開される。
HOGE を参照している箇所は、"hoge" という値が直接バイナリファイルの該当箇所に書き込まれる。)


この仕様は、バイナリ互換性を保つための要件として Java の言語仕様で記述されている。

『The Java Language Specification, Third Edition』
http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html


「13.1 The Form of a Binary」 より
References to fields that are constant variables are resolved at compile time to the constant value that is denoted. No reference to such a constant field should be present in the code in a binary file
(定数のフィールドへの参照はコンパイル時に定数のフィールドが示している値に解決される。そのような参照はバイナリファイルのコード中にあるべきではない。)


言語仕様では、final で宣言され、コンパイル時に初期化されるプリミティブ型もしくは String 型の変数を "定数(constant variable)" と呼んでいる。(「4.12.4 final Variables」 より)
インライン展開を求める理由の1つとして、switch 文の case 値に重複がないことをコンパイル時にチェックするためだと記述されている。(「13.4.9 final Fields and Constants」 より)


定数の値を変更した場合は特に注意。定数フィールドが定義されているクラスはコンパイルされても、その定数を参照しているクラスはコンパイルされない、といったことが起こりえる。Eclipseコンパイラは定数フィールドが変更されたら、それを参照しているクラスを検出して自動でコンパイルするみたいだけど、Sun の javac による差分コンパイルは、ソースファイルとクラスファイルの新旧を比較して、クラスファイルが古い場合に再コンパイルを行うだけ。


この問題の解決策として言語仕様では
・絶対に変更されない値を定数値として使用する。
・アクセス用メソッドを用意する。
を推奨している。

アクセス用メソッドとは、


private static final String HOGE = "hoge";
public static String getHoge() {
return HOGE;
}
のようなもの。めんどくさいけど。

値を変更しない。全部コンパイルし直す。の方が楽ですかね。