遅延初期化(lazy initialization)は、フィールドの値が必要になるまで初期化を遅らせる手法です。 static フィールドとインスタンスフィールドの両方に利用できます。
まず、第一に、通常は遅延初期化を使うべきではありません。 初期化を遅らせる仕組みによって、フィールドへのアクセスコストが増加するからです。
遅延初期化を採用する理由は間違いなく「最適化」が目的ですが、項目55で述べたように多くの「最適化」は失敗します。 パフォーマンスを測定し、ほんとうに遅延初期化が必要かどうかを必ず判断してください。
マルチスレッド環境の遅延初期化
通常の初期化
遅延初期化しない通常の初期化は次のように行われます。
private final FieldType field = initializeField();
これは static フィールドの場合も同様です。 final キーワードがついていることが重要です。
final キーワードはマルチスレッド環境においても安全にフィールドが初期化されることを保証するからです。
通常の遅延初期化
次に最も一般的な遅延初期化の方法を紹介します。
private FieldType fieldType; public synchronized FieldType getField() { if (field == null) { field = initializeField(); } return field; }
これは static フィールドでも、インスタンスフィールドでも有効です。 synchronized キーワードによって適切に同期化されていることが重要です。
最適化された遅延初期化
もし、単に同期化しただけの遅延初期化でパフォーマンス上の問題が発生するならば、いくつかの選択肢があります。
遅延初期化ホルダークラス
static フィールドの初期化には遅延初期化ホルダークラスイディオムを使うと効果的です。
private static class FieldHolder { static final FieldType field = initializeField(); } static public FieldType getField() { return FieldHolder.field; }
これは『クラスが利用されるまでクラスが初期化されない』という言語仕様を利用したイディオムです。
ダブルチェックイディオム
遅延初期化ホルダークラスはシンプルで効果的な実装ですが、インスタンスフィールドには適用できません。
インスタンスフィールドにはダブルチェックイディオムと呼ばれる遅延初期化が有効です。
private volatile FieldType field; public FieldType getField() { FieldType result = field; if (result == null) { synchronized(this) { result = field; if (result == null) { field = result = initializeField(); } } } return result; }
synchronized で同期化する前に初期化されているかどうかをチェックし、この時点で初期化されていたらそのまま result を返します。 こうすることでほとんどの場合、同期化を避けて値を返すことが可能です。
もし、初期化されていない場合には念のために synchronized を使って同期化し、もう一度チェックします。 このように二重にチェックするためにダブルチェックイディオムと呼ばれています。
フィールドが volatile であることは非常に重要です。 また、一度、ローカル変数に代入することでパフォーマンスが改善します。
このイディオムは Java 5 未満のメモリセマンティクスでは動作が保証されないことに注意が必要です。
単一チェックイディオム
もし、特に高いパフォーマンスが求められる場合で、二回以上初期化しても問題ないのであれば synchronized を外してもかまいません。
private volatile FieldType field; public FieldType getField() { FieldType result = field; if (result == null) { field = result = initializeField(); } return result; }
きわどい単一チェックイディオム
もし、long と double 以外の基本データ型であれば、単一チェックイディオムから volatile を取り除いてもかまいません。
private int field; public int getField() { int result = field; if (result == null) { field = result = initializeField(); } return result; }
この手法では、新しいスレッドがアクセスするたびに initializeField() が呼ばれる可能性があります。 スレッドは、volatile 修飾子がなくても自分のスレッドが行った変数の変更は見ることができる一方、他のスレッドの変更は見ることができない可能性があるからです。
これはかなりきわどい手法ですが意図通りに動作します。 実際、String インスタンスで hashCode 値をキャッシュするために利用されています。