Javaにおけるフライウェイトパターン:共有オブジェクトインスタンスによるメモリ効率の最大化
フライウェイトデザインパターンの目的
Javaにおけるフライウェイトデザインパターンは、メモリ使用量の最適化とアプリケーションパフォーマンスの向上に不可欠です。作成されるオブジェクト数を最小限にすることで、メモリフットプリントを大幅に削減します。フライウェイトパターンの主な目的は、類似したオブジェクト間でできるだけ多くのデータを共有することで、効率性とパフォーマンスを向上させることです。
現実世界の例を用いたフライウェイトパターンの詳細な説明
現実世界の例
Javaにおけるフライウェイトパターンの現実世界のアプリケーションとしては、Microsoft WordやGoogle Docsなどのテキストエディタが挙げられます。これらのアプリケーションは、文字オブジェクトを共有することでメモリを効率的に管理し、メモリフットプリントを大幅に削減するためにフライウェイトを使用しています。このようなアプリケーションでは、ドキュメント内の各文字が個別のオブジェクトになる可能性があり、メモリ使用量の点で非常に非効率になります。代わりに、フライウェイトパターンを使用して文字オブジェクトを共有できます。「A」という文字のすべてのインスタンスは、固有の状態(例えば、文字の形状)を持つ単一の「A」オブジェクトを共有できます。外在的な状態(位置、フォント、色など)は別々に保存し、必要に応じて適用できます。このようにして、アプリケーションは複数回出現する文字に対して既存のオブジェクトを再利用することで、メモリを効率的に管理します。
簡単に言うと
類似したオブジェクトでできるだけ多くのものを共有することにより、メモリ使用量や計算コストを最小限に抑えるために使用されます。
Wikipediaによると
コンピュータプログラミングにおいて、フライウェイトはソフトウェアデザインパターンです。フライウェイトは、他の類似したオブジェクトとできるだけ多くのデータを共有することによりメモリ使用量を最小限に抑えるオブジェクトであり、単純な繰り返し表現では受け入れられない量のメモリを使用する可能性がある場合に、大量のオブジェクトを使用する方法です。
Javaにおけるフライウェイトパターンのプログラミング例
錬金術師の店には、魔法薬の瓶が棚いっぱいに並んでいます。多くの薬は同じなので、それぞれに新しいオブジェクトを作成する必要はありません。代わりに、1つのオブジェクトインスタンスが複数の棚のアイテムを表すことができるため、メモリフットプリントは小さく保たれます。
まず、さまざまなPotion
タイプがあります。
public interface Potion {
void drink();
}
@Slf4j
public class HealingPotion implements Potion {
@Override
public void drink() {
LOGGER.info("You feel healed. (Potion={})", System.identityHashCode(this));
}
}
@Slf4j
public class HolyWaterPotion implements Potion {
@Override
public void drink() {
LOGGER.info("You feel blessed. (Potion={})", System.identityHashCode(this));
}
}
@Slf4j
public class InvisibilityPotion implements Potion {
@Override
public void drink() {
LOGGER.info("You become invisible. (Potion={})", System.identityHashCode(this));
}
}
次に、薬を作成するためのファクトリである実際のフライウェイトクラスPotionFactory
があります。
public class PotionFactory {
private final Map<PotionType, Potion> potions;
public PotionFactory() {
potions = new EnumMap<>(PotionType.class);
}
Potion createPotion(PotionType type) {
var potion = potions.get(type);
if (potion == null) {
switch (type) {
case HEALING -> potion = new HealingPotion();
case HOLY_WATER -> potion = new HolyWaterPotion();
case INVISIBILITY -> potion = new InvisibilityPotion();
default -> {
}
}
potions.put(type, potion);
}
return potion;
}
}
AlchemistShop
には、魔法薬の瓶が2つの棚にあります。薬は前述のPotionFactory
を使用して作成されます。
@Slf4j
public class AlchemistShop {
private final List<Potion> topShelf;
private final List<Potion> bottomShelf;
public AlchemistShop() {
var factory = new PotionFactory();
topShelf = List.of(
factory.createPotion(PotionType.INVISIBILITY),
factory.createPotion(PotionType.INVISIBILITY),
factory.createPotion(PotionType.STRENGTH),
factory.createPotion(PotionType.HEALING),
factory.createPotion(PotionType.INVISIBILITY),
factory.createPotion(PotionType.STRENGTH),
factory.createPotion(PotionType.HEALING),
factory.createPotion(PotionType.HEALING)
);
bottomShelf = List.of(
factory.createPotion(PotionType.POISON),
factory.createPotion(PotionType.POISON),
factory.createPotion(PotionType.POISON),
factory.createPotion(PotionType.HOLY_WATER),
factory.createPotion(PotionType.HOLY_WATER)
);
}
public final List<Potion> getTopShelf() {
return List.copyOf(this.topShelf);
}
public final List<Potion> getBottomShelf() {
return List.copyOf(this.bottomShelf);
}
public void drinkPotions() {
LOGGER.info("Drinking top shelf potions\n");
topShelf.forEach(Potion::drink);
LOGGER.info("Drinking bottom shelf potions\n");
bottomShelf.forEach(Potion::drink);
}
}
私たちのシナリオでは、勇敢な訪問者が錬金術師の店に入り、すべての薬を飲み干します。
public static void main(String[] args) {
// create the alchemist shop with the potions
var alchemistShop = new AlchemistShop();
// a brave visitor enters the alchemist shop and drinks all the potions
alchemistShop.drinkPotions();
}
プログラム出力
09:02:52.731 [main] INFO com.iluwatar.flyweight.AlchemistShop -- Drinking top shelf potions
09:02:52.733 [main] INFO com.iluwatar.flyweight.InvisibilityPotion -- You become invisible. (Potion=1395089624)
09:02:52.733 [main] INFO com.iluwatar.flyweight.InvisibilityPotion -- You become invisible. (Potion=1395089624)
09:02:52.733 [main] INFO com.iluwatar.flyweight.StrengthPotion -- You feel strong. (Potion=1450821318)
09:02:52.733 [main] INFO com.iluwatar.flyweight.HealingPotion -- You feel healed. (Potion=668849042)
09:02:52.733 [main] INFO com.iluwatar.flyweight.InvisibilityPotion -- You become invisible. (Potion=1395089624)
09:02:52.733 [main] INFO com.iluwatar.flyweight.StrengthPotion -- You feel strong. (Potion=1450821318)
09:02:52.733 [main] INFO com.iluwatar.flyweight.HealingPotion -- You feel healed. (Potion=668849042)
09:02:52.733 [main] INFO com.iluwatar.flyweight.HealingPotion -- You feel healed. (Potion=668849042)
09:02:52.733 [main] INFO com.iluwatar.flyweight.AlchemistShop -- Drinking bottom shelf potions
09:02:52.734 [main] INFO com.iluwatar.flyweight.PoisonPotion -- Urgh! This is poisonous. (Potion=2096057945)
09:02:52.734 [main] INFO com.iluwatar.flyweight.PoisonPotion -- Urgh! This is poisonous. (Potion=2096057945)
09:02:52.734 [main] INFO com.iluwatar.flyweight.PoisonPotion -- Urgh! This is poisonous. (Potion=2096057945)
09:02:52.734 [main] INFO com.iluwatar.flyweight.HolyWaterPotion -- You feel blessed. (Potion=1689843956)
09:02:52.734 [main] INFO com.iluwatar.flyweight.HolyWaterPotion -- You feel blessed. (Potion=1689843956)
Javaでフライウェイトパターンを使用するケース
フライウェイトパターンの有効性は、それがどのようにそしてどこで使用されるかに大きく依存します。次のすべてが当てはまる場合に、フライウェイトパターンを適用します。
- フライウェイトパターンは、大量のオブジェクトを使用するJavaアプリケーションで特に効果的です。
- オブジェクトの量のためにストレージコストが高い場合、フライウェイトは固有のデータを共有し、外在的な状態を個別に管理することで役立ちます。
- オブジェクトの状態の大部分は外在的なものにすることができます。
- 外在的な状態が削除されると、多くのオブジェクトのグループを比較的少数の共有オブジェクトで置き換えることができます。
- アプリケーションはオブジェクトの同一性に依存しません。フライウェイトオブジェクトは共有される可能性があるため、同一性テストは概念的に異なるオブジェクトに対してtrueを返します。
Javaにおけるフライウェイトパターンの現実世界のアプリケーション
- java.lang.Integer#valueOf(int)、および同様にByte、Character、その他のラッパー型。
- JavaのStringクラスは、文字列リテラルを効率的に管理するためにフライウェイトパターンを使用しています。
- GUIアプリケーションは、フォントやグラフィカルコンポーネントなどのオブジェクトの共有にフライウェイトを頻繁に使用し、メモリを節約し、パフォーマンスを向上させています。
フライウェイトパターンのメリットとトレードオフ
メリット
- オブジェクトのインスタンス数を減らし、メモリを節約します。
- 状態管理を集中化し、状態の不整合のリスクを軽減します。
トレードオフ
- 共有オブジェクトの管理レイヤーを追加することで複雑さが増します。
- 適切に実装されていない場合、共有オブジェクトへのアクセスにオーバーヘッドが発生する可能性があります。
関連するJavaデザインパターン
- コンポジット:コンポジットが共有可能な場合、フライウェイトとよく組み合わせて使用されます。どちらもオブジェクトの階層と構造を管理するために使用されます。
- 状態:共有フライウェイトオブジェクトの状態を管理し、内部状態(不変)と外部状態(コンテキスト固有)を区別するために使用できます。