Javaでfinal変数が呼ぶ側に埋め込まれる理由

2015年4月26日日曜日

Java

このタイトルはあまり正確じゃない。
正確にはfinal宣言と同時に値が定義された変数である。
Javaではfinal宣言と同時に値が定義されるかされないかで変数の挙動が変わってくる。
たとえば、
class A {
    public static final int a = 12345;
    public static final int b;

    static {
        b = 67890;
    }
}
上記のような場合、aはコンパイル時に値が決定する。これがいわゆる"定数(constant)"でbの場合は実行時にstaticイニシャライザが実行されるまで値が決定されない。
これはいわゆる一度だけ初期化可能な変数(readonly)だ。

ここまでは問題ないし、次もJavaプログラマならよく知っているだろう。

class B {
    public int addA() {
        return A.a + A.b;
    }
}
この場合、A.aはクラスBに埋め込まれてしまう。そのため、aの値を変更した場合、クラスAだけでなく、A.aを参照しているクラス、この場合はクラスBまで再コンパイルが必要となる。
これを嫌ってbのように宣言と同時に初期化せずにstaticイニシャライザで初期化しているコードもある。

なぜ、こんな仕様になっているんだろうか?
参照している側に埋め込まずに実行時にクラスAに参照しにいく仕様ではダメだったのだろうか?
パフォーマンスの問題だろうか?

結論を言ってしまえば、switch文である。
switch (x) {
    case A.a :
        ...
}
は問題ないけど
switch (x) {
    case A.b :
        ...
}
はコンパイルエラーになってしまうのだ。

これはなぜか?
まず、コンパイル時、実行時ともにcaseの値が重複しないことが保証されなければならない。
また、Javaのswitch文はJVMのlookupswitchかtableswitchバイトコードにコンパイルされるけど、Javaに限らず、通常switch-caseではジャンプテーブルが生成されることが多い。
この場合もcaseの値がコンパイル時と実行時で異なってしまうと都合が悪い。

そんな訳で定数は参照しているクラスに埋め込まれてしまうのだ。