java-recipes

ホーム デザインパターン › DP-11

DP-11: Flyweight パターン

大量の細かいオブジェクトを共有してメモリ使用量を削減するパターンです。内部状態(共有可能な情報)と外部状態(インスタンス固有の情報)を分離することで、 オブジェクトの再利用を実現します。

Flyweight パターンとは

テキストエディタで100万文字を表示するとき、各文字ごとにフォント情報(フォントファミリー・サイズ・色)を持つオブジェクトを作成すると、 100万個のオブジェクトが生成されてしまいます。しかし実際には、フォント情報は多くの文字で共通しています。 Flyweight パターンは、こうした共有できる情報(内部状態)をキャッシュして再利用し、文字ごとに異なる情報(外部状態: 描画位置など)は呼び出し側で渡すことで、オブジェクト数を大幅に削減します。

Flyweight パターンの登場人物

  • Flyweight(共有オブジェクト): 内部状態のみを保持し、外部状態は引数で受け取る(例: CharFont)
  • FlyweightFactory(ファクトリ): 同じ内部状態のオブジェクトをキャッシュして共有する(例: FontFactory)
  • 内部状態(intrinsic state): オブジェクト間で共有できる情報(フォントファミリー・サイズ・色)
  • 外部状態(extrinsic state): 各呼び出しごとに異なる情報(文字の種類・描画位置)

Java 標準ライブラリでは Integer.valueOf() が -128〜127 の範囲でインスタンスをキャッシュしており、これも Flyweight パターンの一例です。

サンプルコード

FlyweightPatternSample.java
import java.util.HashMap;
import java.util.Map;

public class FlyweightPatternSample {

    // Flyweight: 共有オブジェクト(内部状態のみ保持)
    static class CharFont {
        private final String fontFamily; // 内部状態(共有)
        private final int fontSize;      // 内部状態(共有)
        private final String color;      // 内部状態(共有)

        CharFont(String fontFamily, int fontSize, String color) {
            this.fontFamily = fontFamily;
            this.fontSize = fontSize;
            this.color = color;
            System.out.println("  新しいフォントオブジェクトを作成: " + this);
        }

        // 外部状態(character: 文字、x/y: 描画位置)を受け取って描画
        void render(char character, int x, int y) {
            System.out.println("  文字'" + character + "' を位置(" + x + "," + y + ")に描画"
                    + " [" + fontFamily + " " + fontSize + "pt " + color + "]");
        }

        @Override
        public String toString() {
            return fontFamily + "/" + fontSize + "pt/" + color;
        }
    }

    // Flyweight Factory: フォントオブジェクトのキャッシュ・共有管理
    static class FontFactory {
        private final Map<String, CharFont> cache = new HashMap<>();

        CharFont getFont(String fontFamily, int fontSize, String color) {
            String key = fontFamily + "_" + fontSize + "_" + color;
            if (!cache.containsKey(key)) {
                cache.put(key, new CharFont(fontFamily, fontSize, color));
            }
            return cache.get(key);
        }

        int getCacheSize() {
            return cache.size();
        }
    }

    public static void main(String[] args) {
        FontFactory factory = new FontFactory();

        // テキスト「Hello」を描画(各文字の位置は外部状態)
        System.out.println("=== テキスト描画開始 ===");
        String text = "Hello";
        for (int i = 0; i < text.length(); i++) {
            // 全文字で同じフォントを共有 → 1つのオブジェクトのみ作成される
            CharFont font = factory.getFont("Arial", 12, "black");
            font.render(text.charAt(i), i * 10, 0);
        }

        System.out.println("\n=== 別フォントのテキスト ===");
        CharFont boldFont = factory.getFont("Arial", 16, "red");
        boldFont.render('!', 50, 0);

        // 同じフォントを再取得 → キャッシュから返す
        CharFont sameFont = factory.getFont("Arial", 12, "black");
        System.out.println("\n同じフォント再取得(キャッシュ使用)");
        sameFont.render('W', 0, 20);

        System.out.println("\n作成されたフォントオブジェクト数: " + factory.getCacheSize());
        System.out.println("(" + (text.length() + 2) + "回の描画に対して " + factory.getCacheSize() + " 個のオブジェクトを共有)");
    }
}

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 オブジェクトのフィールドが変更されないこと(不変性の確認)

GitHub でソースコードを見る →