2-3:privateのコンストラクタかenum型でシングルトン特性を強制する
シングルトンは、単に1度しかインスタンスが作成されないクラス。
クラスをシングルトンにすることは、そのクライアントのテストを困難にする
■シングルトンを実装する方法
①public static final フィールド+private コンストラクタ(簡単な方法)
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
// コンストラクタ処理
}
public void sing() { System.out.println("♪"); }
}
特徴
-
シンプルで API から「シングルトン」であることが明確。
-
クラスロード時にインスタンスが作られる。
-
スレッドセーフ(class initialization が JVM により同期される)。
問題点
-
シリアライズすると復元時に別インスタンスが作られる(後述の対策が必要)。
-
リフレクションで private コンストラクタを呼ばれうる(対策可能だが完璧ではない)。
リフレクションによる二重生成を検出するガード
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private static boolean created = false;
private Elvis() {
if (created) {
throw new IllegalStateException("既にインスタンスが作られています");
}
created = true;
}
}
注意: このガードは Constructor.setAccessible(true) 経由で普通にコンストラクタを呼ぶことは防げるが、sun.reflect.ReflectionFactory 等の低レベルな手段を使えばコンストラクタを通さずにインスタンス作成されるケースも理論上ある(つまり完全無敵ではない)。
②static factory(ホルダ・イディオム) — 遅延初期化が欲しいときの推奨
public class Elvis {
private Elvis() { }
private static class Holder {
static final Elvis INSTANCE = new Elvis();
}
public static Elvis getInstance() {
return Holder.INSTANCE;
}
public void sing() { System.out.println("♪"); }
}
特徴
-
遅延初期化(getInstance() が初めて呼ばれたときにインスタンス化)。
-
スレッドセーフで簡潔(同期不要)。
-
Elvis::getInstance を Supplier として使える柔軟性がある(ファクトリメソッドの利点)。
③シリアライズ可能なシングルトン — readResolve() を使う方法
シリアライズするとデフォルトの挙動は デシリアライズ毎に新しいインスタンス を作る。
シングルトン性を維持するために readResolve を実装する。
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
// デシリアライズ時にシングルトン INSTANCE を返す
private Object readResolve() {
return INSTANCE;
}
}
要点
implements Serializable するだけではダメ。
readResolve() を提供して常に既存のインスタンスを返す必要がある。
④enum を使ったシングルトン(Effective Java の推奨)
public enum Elvis {
INSTANCE;
public void sing() {
System.out.println("♪");
}
}
特徴
- 実装が最も単純で安全性が高い(シリアライズとリフレクションに対する堅牢性を JVM が保証する)。
・シリアライズ:enum は JVM が正しく扱うため、復元時に別インスタンスにならない。
・リフレクション:Class.getDeclaredConstructors() 経由でコンストラクタにアクセスしても JVM が enum のインスタンスを増やすことを許さない(例外が出る)。
- クロス-クラスローダの状況でも単一性が保たれやすい。
短所 / 制約
-
enum は他クラスを継承できない(extends 不可)。(ただしインタフェースは実装可能)
-
通常は イager(クラスロード時に定数が作られる) — 完全な遅延初期化はできない(ただし複雑な工夫は可能だが本来の用途から外れる)
-
API 上でも Elvis.INSTANCE という形で利用されるので、getInstance() のような柔軟な署名(Supplier としての活用など)が使いづらい場合がある。
まとめ
堅牢さ(シリアライズ・リフレクション耐性)を最も重視するなら enum がベストプラクティス。
いつどれを選ぶか
-
最も安全で簡単にしたければ → enum シングルトン(Effective Java の推奨)。
-
遅延初期化が必要で柔軟性を残したい → ホルダ・イディオム(static factory)。
-
外部から new を完全に防ぎたい/シングルトンであることを API に明示したい
→ public static final フィールド(シンプルだがシリアライズ対策・リフレクション対策を忘れずに)。
最後に
-
enum はシリアライズとリフレクションに対して最も安全で、Effective Java ではこれを推奨している。
-
public static final / static factory は目的に応じて使い分ける(API の明確さ vs 遅延初期化)。
-
シリアライズ可能にする場合は readResolve()(非 enum)を忘れない。
-
リフレクション対策はできるが完全ではないため、厳密な一意性を求めるなら enum が良い。
-
実務では単純なグローバル依存(シングルトン)より DI の方がテストしやすいことが多い。