Java言語仕様§12.4「クラスやインタフェースの初期化」には次のように記されている[JLS 2005]。
クラスの初期化(initialization)とは、その静的初期化子とそのクラスの中に宣言されているstaticフィールド(クラス変数)に対する初期化子の実行からなる処理である。
言い換えると、staticフィールドが存在すると、クラスの初期化が開始されるということである。しかし、staticフィールドはクラスが初期化されていることに依存する場合が考えられ、その場合、初期化が循環することになる。Java言語仕様§8.3.2.1「クラス変数の初期化子」には次のように記されている。
static変数がfinalとして宣言されており、かつコンパイル時の定数値で初期化される場合、それは実行時に最初に初期化される。
この一文は誤解を招くおそれがある。後で初期化されるstatic finalフィールドの値を使用するインスタンスには当てはまらないからである。フィールドを static final 宣言するだけでは、値が読み取られる前に完全に初期化されることを保証できない。
プログラム一般、セキュリティに配慮すべきプログラムでは特に、クラスの初期化を循環させてはならない。
違反コード (クラス内循環)
以下の違反コード例には、クラス内の初期化循環がある。
public class Cycle { private final int balance; private static final Cycle c = new Cycle(); private static final int deposit = (int) (Math.random() * 100); // ランダムな金額を口座に預金 public Cycle() { balance = deposit - 10; // 残高から手数料を引く } public static void main(String[] args) { System.out.println("The account balance is: " + c.balance); } }
Cycleクラスでは private static final クラス変数を宣言しており、このクラス変数はCycleクラスのインスタンスに初期化される。静的初期化子(static initializer)は、static 宣言されたクラスメンバやコンストラクタの最初の呼び出しより前に1度だけ呼び出されることが保証されている。
プログラマの意図は、預金額(deposit)から手数料(10)を引き、口座の残額(balance)を計算することにある。しかし、クラス変数cの初期化はdepositフィールドが初期化される前に行われる。なぜなら、クラス変数cの初期化はソースコード上、depositフィールドより前に現れるからである。したがって、cの静的初期化時に呼び出されるコンストラクタから見えるdepositの値は、初期値0であって、ランダムな値ではない。結果として、残額(balance)の計算結果は常に-10になってしまう。
Java言語仕様では、このような再帰的な初期化の循環を実装上無視してもよいと認めている。[Bloch 2005]
適合コード (クラス内循環)
以下の適合コードでは、クラスCycleのメンバフィールドの初期化順序を変更し、初期化の循環を発生させずにフィールドが初期化されるようにしている。具体的には、ソースコード上、cの初期化をdepositの初期化の後に置き、depositの初期化が完了した後でクラスが初期化されるようにしている。
public class Cycle { private final int balance; private static final int deposit = (int) (Math.random() * 100); // Random deposit private static final Cycle c = new Cycle(); // 必要なフィールドが初期化された後で実行される public Cycle() { balance = deposit - 10; // 残高から手数料を引く } public static void main(String[] args) { System.out.println("The account balance is: " + c.balance); } }
このような初期化循環の問題は、関係するフィールドが増えるほど気づかれにくくなる。したがって、プログラムの制御フローにこのような循環が発生しないようにすることは重要である。
上述の適合コードのように修正すれば初期化循環は防げるが、宣言の順序に依存しており脆弱である。コードを後日メンテナンスするプログラマは、プログラムが正しく動作するにはこの宣言順序を保たなくてはならないことに気づかないかもしれない。したがって、このような依存性が存在することはコードの中にはっきりコメントとして残しておくべきである。
違反コード (クラス間循環)
以下の違反コード例では、静的変数を持つ2つのクラスを宣言しており、これらの変数は互いに依存している。2つのクラスをあわせて眺めると循環が存在することは明白だが、別々に眺めれば容易に見落としてしまうであろう。
class A { public static final int a = B.b + 1; // ... } class B { public static final int b = A.a + 1; // ... }
クラスの初期化順序が異なれば、計算されるA.aとB.bの値も異なる。クラスAが先に初期化される場合、A.aの値は2になり、B.bの値は1になる。クラスBが先に初期化される場合、値は逆になる。
適合コード (クラス間循環)
以下の適合コードでは、変数の依存関係を解消することでクラス間の循環を断ち切っている。
class A { public static final int a = 2; // ... } // class B は変更なし: b = A.a + 1
循環が発生しないため、どちらのクラスが先に初期化されるかに関係なく、初期値は常にA.a = 2とB.b = 3になる。
リスク評価
初期化循環は予期せぬ結果をもたらす可能性がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
DCL00-J | 低 | 低 | 中 | P2 | L3 |
関連ガイドライン
The CERT C++ Secure Coding Standard | DCL14-CPP. Avoid assumptions about the initialization order between translation units |
ISO/IEC TR 24772:2010 | Initialization of Variables [LAV] |
参考文献
[JLS 2005] | §8.3.2.1, Initializers for Class Variables |
§12.4, Initialization of Classes and Interfaces | |
[Bloch 2005] | Puzzle 49: Larger than life |
[MITRE 2009] | CWE-665. Improper initialization |
翻訳元
これは以下のページを翻訳したものです。
DCL00-J. Prevent class initialization cycles (revision 83)