DP-11: Flyweight パターン
大量の細かいオブジェクトを共有してメモリ使用量を削減するパターンです。内部状態(共有可能な情報)と外部状態(インスタンス固有の情報)を分離することで、 オブジェクトの再利用を実現します。
Flyweight パターンとは
テキストエディタで100万文字を表示するとき、各文字ごとにフォント情報(フォントファミリー・サイズ・色)を持つオブジェクトを作成すると、 100万個のオブジェクトが生成されてしまいます。しかし実際には、フォント情報は多くの文字で共通しています。 Flyweight パターンは、こうした共有できる情報(内部状態)をキャッシュして再利用し、文字ごとに異なる情報(外部状態: 描画位置など)は呼び出し側で渡すことで、オブジェクト数を大幅に削減します。
Flyweight パターンの登場人物
- Flyweight(共有オブジェクト): 内部状態のみを保持し、外部状態は引数で受け取る(例: CharFont)
- FlyweightFactory(ファクトリ): 同じ内部状態のオブジェクトをキャッシュして共有する(例: FontFactory)
- 内部状態(intrinsic state): オブジェクト間で共有できる情報(フォントファミリー・サイズ・色)
- 外部状態(extrinsic state): 各呼び出しごとに異なる情報(文字の種類・描画位置)
Java 標準ライブラリでは Integer.valueOf() が -128〜127 の範囲でインスタンスをキャッシュしており、これも Flyweight パターンの一例です。
サンプルコード
Java 8 では HashMap を使ってフォントオブジェクトをキャッシュします。同じ内部状態(フォントファミリー・サイズ・色)を持つオブジェクトは1つだけ作成され、何度でも共有されます。
よくあるミス・注意点
⚠️ 共有オブジェクトの状態を変更してしまう
Flyweight オブジェクトは複数の場所から共有されるため、状態を変更するとすべての利用箇所に影響が出ます。 Flyweight は必ず不変(immutable)に設計し、フィールドはすべて final にしましょう。
⚠️ マルチスレッド環境での HashMap の使用
複数スレッドから FlyweightFactory にアクセスする場合、 HashMap はスレッドセーフではありません。 マルチスレッド環境では ConcurrentHashMap を使用しましょう。
⚠️ String.intern() の多用は逆効果になることがある
String.intern() は JVM の文字列プールを使った Flyweight の仕組みですが、プールに大量の文字列を追加すると PermGen(Java 8 以前)や Metaspace(Java 8 以降)を圧迫する恐れがあります。 むやみに使うのは避け、明示的なキャッシュ管理を検討しましょう。
⚠️ キャッシュが際限なく増え続ける
キャッシュに追加するだけで削除しない設計だと、長時間稼働するアプリケーションでメモリリークになります。 利用頻度の低いオブジェクトを削除する仕組み( WeakHashMap や LRU キャッシュ)の導入も検討しましょう。
テストする観点
- 同じ内部状態(フォントファミリー・サイズ・色)で
getFont()を2回呼んだとき、 同一インスタンスが返ること(==で確認) - 異なる内部状態で呼んだとき、異なるインスタンスが返ること
- N 回描画しても、キャッシュサイズが内部状態の種類数と一致すること(オブジェクト数の節約確認)
- 外部状態(描画位置)を変えても、同じ Flyweight オブジェクトが再利用されること
- Flyweight オブジェクトのフィールドが変更されないこと(不変性の確認)