概要
テキストエディタで1文字ごとにフォント情報を持つオブジェクトを生成すると、数万文字のドキュメントで膨大なメモリを消費します。しかし実際に使われるフォントの組み合わせは数種類程度で、大半のオブジェクトは同じ属性を持っています。Flyweight パターンは、共有可能な「内部状態(intrinsic state)」をキャッシュで使い回し、オブジェクトごとに異なる「外部状態(extrinsic state)」はメソッド引数で渡す構造を作ります。Java 標準ライブラリでも Integer.valueOf がキャッシュ(-128〜127)を使って同じ考え方を適用しています。この記事ではフォント描画を題材に Flyweight の構造を示し、Java 17 の record で Flyweight を不変オブジェクトとして定義する方法を確認します。
使いどころ
テキストエディタでフォント情報(書体・サイズ・色)を共有し、数万文字の描画でもメモリ消費を一定に抑える
地図アプリケーションでアイコン画像(種別・サイズ)を共有し、数千のピンを表示してもメモリを節約する
ゲームの粒子エフェクトでテクスチャや色の組み合わせを共有し、大量のパーティクル生成を効率化する
コード例
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
}
}Version Coverage
record で Flyweight を定義すると equals・hashCode・toString が自動生成され、不変性も保証される。computeIfAbsent でキャッシュ取得も簡潔。
// 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
注意点
Flyweight は内部状態が不変であることが前提。可変な状態を共有すると、一箇所の変更が全参照に波及する
キャッシュの Map がクリアされない場合、メモリリークの原因になる。長時間稼働するアプリでは WeakHashMap やサイズ制限付きキャッシュを検討する
外部状態をメソッド引数で渡す設計は、呼び出し側の責任が増える。外部状態の管理が複雑になる場合は Flyweight の適用を見直す
Integer.valueOf の -128〜127 のキャッシュは Flyweight の典型例。== で比較すると範囲外の値で予期しない結果になるため、equals を使うこと
FAQ
共有できるオブジェクトの割合に依存します。フォント描画の例では、数万文字でも実際のフォントオブジェクトは数個で済むため、99%以上のメモリ削減が見込めます。
はい。-128〜127 の Integer オブジェクトをキャッシュで共有する設計は Flyweight の典型例です。この範囲外では新しいオブジェクトが生成されます。
同じ考え方です。String.intern() はプール内の同一文字列を共有し、メモリ消費を抑えます。ただし大量の文字列をインターンするとプール自体が膨らむため注意が必要です。