概要

テキストエディタで1文字ごとにフォント情報を持つオブジェクトを生成すると、数万文字のドキュメントで膨大なメモリを消費します。しかし実際に使われるフォントの組み合わせは数種類程度で、大半のオブジェクトは同じ属性を持っています。Flyweight パターンは、共有可能な「内部状態(intrinsic state)」をキャッシュで使い回し、オブジェクトごとに異なる「外部状態(extrinsic state)」はメソッド引数で渡す構造を作ります。Java 標準ライブラリでも Integer.valueOf がキャッシュ(-128〜127)を使って同じ考え方を適用しています。この記事ではフォント描画を題材に Flyweight の構造を示し、Java 17 の record で Flyweight を不変オブジェクトとして定義する方法を確認します。

使いどころ

テキストエディタでフォント情報(書体・サイズ・色)を共有し、数万文字の描画でもメモリ消費を一定に抑える

地図アプリケーションでアイコン画像(種別・サイズ)を共有し、数千のピンを表示してもメモリを節約する

ゲームの粒子エフェクトでテクスチャや色の組み合わせを共有し、大量のパーティクル生成を効率化する

コード例

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

public class FlyweightPatternSample {

    // Flyweight: record で不変オブジェクトとして定義(Java 17+)
    record CharFont(String fontFamily, int fontSize, String color) {
        void render(char character, int x, int y) {
            System.out.println("  '" + character + "' at (" + x + "," + y
                + ") [" + fontFamily + " " + fontSize + "pt " + color + "]");
        }
    }

    // Flyweight Factory: キャッシュで共有管理
    static class FontFactory {
        private final Map<String, CharFont> cache = new HashMap<>();

        CharFont getFont(String family, int size, String color) {
            var key = family + "_" + size + "_" + color;
            return cache.computeIfAbsent(key,
                k -> new CharFont(family, size, color));
        }

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

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

        // 同じフォントを共有して描画
        var text = "Hello";
        for (int i = 0; i < text.length(); i++) {
            var font = factory.getFont("Arial", 12, "black");
            font.render(text.charAt(i), i * 10, 0);
        }

        // 別フォントを取得
        var bold = factory.getFont("Arial", 16, "red");
        bold.render('!', 50, 0);

        System.out.println("\nオブジェクト数: " + factory.getCacheSize());
        System.out.println("描画回数: " + (text.length() + 1));

        // record の equals で同一性を確認
        var f1 = factory.getFont("Arial", 12, "black");
        var f2 = factory.getFont("Arial", 12, "black");
        System.out.println("同一インスタンス: " + (f1 == f2)); // true
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で Flyweight を定義すると equals・hashCode・toString が自動生成され、不変性も保証される。computeIfAbsent でキャッシュ取得も簡潔。

Java 17
// Java 17: record で不変 Flyweight を定義
record CharFont(String family, int size, String color) {
    void render(char c, int x, int y) {
        System.out.println(c + " at (" + x + "," + y + ")");
    }
}
// computeIfAbsent でキャッシュ取得
return cache.computeIfAbsent(key,
    k -> new CharFont(family, size, color));

Library Comparison

標準 API(HashMap + record)共有対象の属性が少なく、キャッシュを自前で管理できるとき。キャッシュのサイズ管理やクリア戦略は自前で実装する必要がある。
Guava Cache(CacheBuilder)サイズ上限・有効期限・自動削除などキャッシュの高度な管理が必要なとき。Guava 全体を依存に追加する必要がある。単純な Flyweight には過剰。
Caffeine高性能キャッシュが必要で、Guava Cache よりも高いスループットを求めるとき。単純なオブジェクト共有だけなら HashMap + computeIfAbsent で十分。

注意点

Flyweight は内部状態が不変であることが前提。可変な状態を共有すると、一箇所の変更が全参照に波及する

キャッシュの Map がクリアされない場合、メモリリークの原因になる。長時間稼働するアプリでは WeakHashMap やサイズ制限付きキャッシュを検討する

外部状態をメソッド引数で渡す設計は、呼び出し側の責任が増える。外部状態の管理が複雑になる場合は Flyweight の適用を見直す

Integer.valueOf の -128〜127 のキャッシュは Flyweight の典型例。== で比較すると範囲外の値で予期しない結果になるため、equals を使うこと

FAQ

Flyweight パターンはどのくらいメモリを節約できますか。

共有できるオブジェクトの割合に依存します。フォント描画の例では、数万文字でも実際のフォントオブジェクトは数個で済むため、99%以上のメモリ削減が見込めます。

Integer.valueOf のキャッシュは Flyweight パターンですか。

はい。-128〜127 の Integer オブジェクトをキャッシュで共有する設計は Flyweight の典型例です。この範囲外では新しいオブジェクトが生成されます。

String のインターンプールも Flyweight ですか。

同じ考え方です。String.intern() はプール内の同一文字列を共有し、メモリ消費を抑えます。ただし大量の文字列をインターンするとプール自体が膨らむため注意が必要です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます