「オブジェクトの生成処理が重くてパフォーマンスが低下している」「複雑な初期設定を持つインスタンスを使い回したい」といった悩みを抱えていませんか?本記事では、そのような課題を解決する「Prototypeprototype [プロトタイプ]動作確認や検証のために作成される試作品(プロトタイプ)パターン」について、JavaJava [ジャバ]オブジェクト指向プログラミング言語での具体的な実装例を交えてわかりやすく解説します。
記事のポイント
- Prototypeパターンは、既存のインスタンスをコピー(複製)して新しいインスタンスを生成するデザインパターンです。
- 生成コストが高いオブジェクトを毎回ゼロから構築するのを避け、初期化処理のコストを削減できる場合があります。
- Javaでは
Cloneableインターフェースとclone()メソッドを使う方法が有名ですが、近年はコピーコンストラクタを利用する安全な手法も推奨されます。- 複製時には「シャローコピー(浅いコピー)」と「ディープコピー(深いコピー)」の違いを理解することが重要です。
JavaにおけるPrototype(プロトタイプ)パターンとは?
Prototype(プロトタイプ) パターンは、GoF(Gang of Four)によって定義された23のデザインパターンのうち、「生成に関するパターン」の1つです。
通常のオブジェクト指向プログラミングでは、new キーワードを使用してクラスからインスタンスをゼロから生成します。しかし、Prototypeパターンでは、あらかじめ用意しておいた「原型(プロトタイプ)」となるインスタンスを**複製(コピー)**することで、新しいオブジェクトを作り出します。
PrototypeパターンをJavaで使うべき場面とメリット
- インスタンス生成のコストが高い場合: データベースからのデータ取得や複雑な計算など、オブジェクトの初期化に時間がかかる場合、毎回ゼロから構築するより、初期化済みのオブジェクトをコピーすることで処理を効率化できる場合があります。
- 実行時にクラスが決まる場合: クライアント側が具体的なクラス名を知らなくても、手元にあるインスタンス(インターフェース)をコピーするだけで同じ型のインスタンスを生成できます。
- 似たような状態を持つオブジェクトを大量に作る場合: 初期化コストがコピー自体のコストを上回る場合、初期設定が済んだオブジェクトのコピーを作り、一部だけ変更する方が効率的です。
Java Prototypeパターンの実装例(Cloneableインターフェース)
Javaには、インスタンスを複製する標準的な仕組みとして Cloneable インターフェースと Object.clone() メソッドが用意されています。
まずは、この標準機能を使った実装例を見てみましょう。
1. Prototypeの定義と実装(cloneメソッド)
モンスター(Monster)を複製して大量にスポーンさせるゲームの仕組みを考えてみます。
// 複製可能であることを示すために Cloneable を実装public class Monster implements Cloneable { private String name; private int hp;
public Monster(String name, int hp) { // ここに重い処理(DBアクセスやアセットのロードなど)があると仮定 try { Thread.sleep(1000); // 1秒かかる重い処理のシミュレート } catch (InterruptedException e) { e.printStackTrace(); } this.name = name; this.hp = hp; }
public void setHp(int hp) { this.hp = hp; }
public void showStatus() { System.out.println("Monster: " + name + ", HP: " + hp); }
// cloneメソッドをオーバーライドして公開(publicにする) @Override public Monster clone() { try { // Objectクラスのclone()を呼び出して自身を複製 return (Monster) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } }}2. クライアント側での呼び出しと実行例
new を使った場合と、clone() を使った場合の挙動の違いを見てみます。
public class Main { public static void main(String[] args) { System.out.println("--- 最初のモンスターを生成 (new) ---"); long start1 = System.currentTimeMillis(); Monster originalSlime = new Monster("スライム", 100); long end1 = System.currentTimeMillis(); System.out.println("生成時間: " + (end1 - start1) + "ms"); originalSlime.showStatus();
System.out.println("\n--- モンスターを複製 (clone) ---"); long start2 = System.currentTimeMillis(); // 原型(プロトタイプ)から複製。重いコンストラクタは呼ばれない! Monster clonedSlime1 = originalSlime.clone(); Monster clonedSlime2 = originalSlime.clone(); long end2 = System.currentTimeMillis(); System.out.println("複製時間: " + (end2 - start2) + "ms");
// 複製したモンスターの値を変更しても原型には影響しない clonedSlime1.setHp(80); clonedSlime2.setHp(50);
originalSlime.showStatus(); clonedSlime1.showStatus(); clonedSlime2.showStatus(); }}実行結果のイメージ:
clone() による単純なコピーは、重い初期化処理を繰り返す必要がないため、今回の例では new より大幅に短時間で完了します。これは、Object.clone() がコンストラクタを呼び出さず、新しいインスタンスを作成してフィールドの値をコピーするためです。
※補足:上記の Monster クラスの例では、フィールドが String(不変オブジェクト)と int(基本データ型)のみであるため、シャローコピーによる問題は発生しません。しかし、リストなどの変更可能な参照型フィールドを持つ場合は注意が必要です。詳しくは次のセクションで解説します。
Javaでclone()を使う際の注意点:シャローコピーとディープコピー
Object.clone() メソッドを使用する際に必ず理解しておかなければならないのが、**シャローコピー(浅いコピー)**の問題です。
super.clone() は、フィールドの値をそのままコピーします。
- 基本データ型(
intやbooleanなど)は値そのものがコピーされるので問題ありません。 - しかし、参照型(配列やList、他のオブジェクトなど)の場合、参照(メモリアドレス)だけがコピーされてしまいます。
つまり、複製したオブジェクトが持つリストを変更すると、元のオブジェクトのリストも変更されてしまう(共有されている状態になる)というバグを引き起こしやすくなります。これを防ぐためには、開発者が手動で参照先のオブジェクトも新しく生成してコピーする**ディープコピー(深いコピー)**を実装する必要があります。
コピーコンストラクタを使ったモダンなJava Prototype実装
Java界隈(特にEffective Javaなどのベストプラクティス)では、Cloneable インターフェースには設計上の欠陥が多いと指摘されており、代わりにコピーコンストラクタやコピーファクトリーを使用することが推奨されています。
public class Player { private String name; private List<String> items;
// 通常のコンストラクタ public Player(String name, List<String> items) { this.name = name; this.items = new ArrayList<>(items); }
// コピーコンストラクタ(自分自身と同じ型を引数に取る) public Player(Player prototype) { this.name = prototype.name; // List自体を新しく作成する。 // Stringは不変オブジェクトなので、要素のコピーは不要。 this.items = new ArrayList<>(prototype.items); }
// コピーファクトリーメソッドの場合 public static Player newInstance(Player prototype) { return new Player(prototype); }}// 利用側Player original = new Player("勇者", Arrays.asList("剣", "盾"));Player cloned = new Player(original); // コピーコンストラクタで複製ここでは List 自体を新しく作成しています。リストの要素である String は不変オブジェクトなので、要素まで個別に再帰的コピーをする必要はありません。
コピーコンストラクタには以下のようなメリットがあります。
Cloneableのようなマーカーインターフェースや例外(CloneNotSupportedException)の処理が不要。finalフィールドとも相性が良い(オーバーライドしたcloneメソッド内で、ディープコピーのためにfinalな参照型フィールドへ新しいオブジェクトを再代入しようとしてもコンパイルエラーになるという制約を受けないため)。- コピー用のコンストラクタでは、必要に応じて別の型やインターフェースから値を取り込むコンストラクタを用意することもできます。ただし、一般に「コピーコンストラクタ」と呼ばれるのは、同じクラスのインスタンスを引数に取る形式です。
まとめ:JavaのPrototypeパターンでオブジェクト生成を最適化
Prototypeパターンは、 「新しいインスタンスを作るために、既存のインスタンスをコピーする」 というシンプルなアイデアですが、オブジェクトの初期化コストが高い場合には、生成処理を効率化できる可能性があります。
- オブジェクトの初期化処理(コンストラクタ)が非常に重い場合。
- 複雑な状態を保ったままのオブジェクトの「スナップショット」を作りたい場合。
このような場面では、Prototypeパターンの導入を検討してみてください。
また、Javaで実装する際は、手軽な Cloneable / clone() だけでなく、安全で確実なコピーコンストラクタの手法もあわせて覚えておくと、バグの少ない堅牢な設計ができるようになります。
以上で本記事の解説を終わります。
よいITライフを!
人気記事
- 1
- 2
- 3
- 4
- 5
思考の整理というテーマを、実務ですぐに使える具体的な「技術」として落とし込んだ一冊です。