はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回は、生成に関するパターンの最後、Prototype(プロトタイプ)パターンを、Javaのサンプルコードとともに解説します。
解決したい課題
「既に存在する、あるオブジェクトとほぼ同じ内容のオブジェクトを、もう1つ作りたい」という場面があります。
単純にnewで新規生成し、フィールドを1つずつコピーするコードを書いてもよいのですが、以下のような課題があります。
- 対象クラスの内部構造(フィールドの数や型)を、コピーする側が詳しく知っている必要がある
- 生成コストが高いオブジェクト(重い初期化処理、DB問い合わせを伴う初期化など)の場合、
newによる再生成が非効率 - 実行時にならないと具体的なクラスが分からない場合(インターフェース経由でしかオブジェクトを扱えない場合)、
new SomeConcreteClass()と直接書くこと自体ができない
Prototypeパターンは、オブジェクト自身に「自分自身を複製する」責任を持たせることで、これらの課題を解決します。
呼び出し側は対象クラスの内部構造を知らなくても、clone()を呼ぶだけで複製が手に入ります。
クラス構成
- Prototype(プロトタイプ): 自分自身を複製するメソッド(
clone())を宣言するインターフェース - ConcretePrototype(具体的なプロトタイプ): Prototypeを実装し、実際の複製処理を行うクラス
Javaでは、標準で用意されているCloneableインターフェースとObject#clone()メソッドを使ってこのパターンを実現するのが一般的です。
Javaでの実装例
シャローコピー(浅いコピー)の例
まずは、フィールドがプリミティブ型や文字列(イミュータブル)だけの、シンプルなケースを見てみます。
public class Sheep implements Cloneable {
private String name;
private int age;
public Sheep(String name, int age) {
this.name = name;
this.age = age;
}
// Object#clone()をオーバーライドして公開する
@Override
public Sheep clone() {
try {
return (Sheep) super.clone();
} catch (CloneNotSupportedException e) {
// Cloneableを実装していれば、このExceptionは実質発生しない
throw new AssertionError("クローンに失敗しました", e);
}
}
@Override
public String toString() {
return "Sheep{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) {
Sheep dolly = new Sheep("ドリー", 1);
Sheep clonedSheep = dolly.clone();
System.out.println(dolly);
System.out.println(clonedSheep);
System.out.println(dolly == clonedSheep); // false(別インスタンス)
}
}
実行結果:
Sheep{name='ドリー', age=1}
Sheep{name='ドリー', age=1}
false
dollyとclonedSheepは、内容は同じですが別のインスタンスです。
new Sheep("ドリー", 1)と自分でコンストラクタを呼ぶのではなく、既存のオブジェクトに「自分を複製して」と依頼するだけで、同じ内容のコピーが手に入ります。
落とし穴:シャローコピーと参照型フィールド
Object#clone()が行うのはシャローコピー(浅いコピー)、つまり「フィールドの値をそのままコピーする」処理です。
フィールドが参照型(他のオブジェクトへの参照)の場合、コピー元とコピー先が同じオブジェクトを参照してしまうという落とし穴があります。
import java.util.ArrayList;
import java.util.List;
public class Sheep implements Cloneable {
private String name;
private List<String> tags; // 参照型フィールド
public Sheep(String name, List<String> tags) {
this.name = name;
this.tags = tags;
}
public void addTag(String tag) {
this.tags.add(tag);
}
@Override
public Sheep clone() {
try {
return (Sheep) super.clone(); // シャローコピー: tagsの参照だけコピーされる
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() {
return "Sheep{name='" + name + "', tags=" + tags + "}";
}
}
public class Main {
public static void main(String[] args) {
Sheep original = new Sheep("ドリー", new ArrayList<>(List.of("元気")));
Sheep cloned = original.clone();
cloned.addTag("複製された個体"); // クローン側にだけタグを追加したつもり
System.out.println("original = " + original);
System.out.println("cloned = " + cloned);
}
}
実行結果:
original = Sheep{name='ドリー', tags=[元気, 複製された個体]}
cloned = Sheep{name='ドリー', tags=[元気, 複製された個体]}
clonedにしかタグを追加していないはずなのに、original側にも反映されてしまいました。
これは、tagsフィールドが同じArrayListインスタンスへの参照をコピー元・コピー先の両方が持っているために起こります。
解決策:ディープコピー(深いコピー)
この問題を避けるには、参照型フィールドについては、参照だけでなく中身も複製する(ディープコピー)必要があります。
import java.util.ArrayList;
import java.util.List;
public class Sheep implements Cloneable {
private String name;
private List<String> tags;
public Sheep(String name, List<String> tags) {
this.name = name;
this.tags = tags;
}
public void addTag(String tag) {
this.tags.add(tag);
}
@Override
public Sheep clone() {
try {
Sheep cloned = (Sheep) super.clone();
// 参照型フィールドは、明示的に新しいインスタンスとしてコピーし直す
cloned.tags = new ArrayList<>(this.tags);
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() {
return "Sheep{name='" + name + "', tags=" + tags + "}";
}
}
実行結果(修正後)
original = Sheep{name='ドリー', tags=[元気]}
cloned = Sheep{name='ドリー', tags=[元気, 複製された個体]}
clone()メソッドの中でtagsフィールドをnew ArrayList<>(this.tags)として明示的に複製し直すことで、コピー元とコピー先が別々のリストを持つようになり、意図通りの独立したコピーが得られます。
フィールドに参照型が含まれるクラスでPrototypeパターンを使う際は、シャローコピーで十分か、ディープコピーが必要かを必ず意識する必要があります。
Java標準ライブラリでの注意点
JavaのCloneable/clone()の仕組みは、実は言語設計上の問題が多いことでも知られています(Cloneableはメソッドを持たないマーカーインターフェースであり、clone()自体はObjectが持つprotectedメソッドという不自然な構造など)。
そのため実務では、clone()を使わずに、コピーコンストラクタ(引数として自分自身の型を受け取るコンストラクタ)や、専用のファクトリメソッドでPrototypeパターン相当のことを実現するケースも多く見られます。
public class Sheep {
private String name;
private List<String> tags;
public Sheep(String name, List<String> tags) {
this.name = name;
this.tags = tags;
}
// コピーコンストラクタによる複製(clone()を使わない代替手段)
public Sheep(Sheep source) {
this.name = source.name;
this.tags = new ArrayList<>(source.tags); // ここでもディープコピーを意識する
}
}
Sheep original = new Sheep("ドリー", new ArrayList<>(List.of("元気")));
Sheep copy = new Sheep(original); // コピーコンストラクタで複製
使用場面
- 生成コストの高いオブジェクト(重い計算処理やDBアクセスを伴う初期化など)を、複製によって効率的に量産したい場合
- 実行時に決まる具体的なクラス(サブクラス)を、その型を意識せずに複製したい場合
- 「ひな形となるオブジェクト」をいくつか用意しておき、それらを複製・微調整して様々なバリエーションを作りたい場合(ゲームにおけるキャラクター・アイテムの複製生成など)
- オブジェクトの状態を「スナップショット」として保持し、後で複製して復元したい場合
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 既存オブジェクトと同じ内容のオブジェクトを、内部構造を知らずに、効率よく複製したい |
| 実現方法 | オブジェクト自身に複製処理(clone()など)を持たせる |
| 注意点 |
Object#clone()はシャローコピーのため、参照型フィールドはディープコピーの要否を個別に検討する必要がある |
| 実務での代替 |
clone()の代わりに、コピーコンストラクタやファクトリメソッドで同等のことを実現するケースも多い |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


