java-recipes

ホーム デザインパターン › DP-04

DP-04: Prototype パターン

既存のオブジェクトを雛形として複製し、新しいインスタンスを作るパターンです。 コスト高な初期化を繰り返さず、雛形から複製することで効率的にオブジェクトを生成できます。

Prototype パターンとは

Prototype(プロトタイプ)パターンは、すでに存在するオブジェクトを「雛形(プロトタイプ)」として使い、 そのコピーから新しいオブジェクトを作るパターンです。 毎回ゼロからオブジェクトを生成するよりも、雛形を複製して必要な部分だけ変更するほうが コードが簡潔になる場面で役立ちます。

Prototype パターンが役立つ場面

  • 雛形からの派生: 「標準注文」を雛形にして「急便注文」「まとめ発注」を作るような場面
  • 設定の共通化: 共通の設定値を持つオブジェクトを複数作りたい場面
  • イミュータブル設計との相性: Java 17+ の record では withXxx() スタイルのコピーが自然に書ける

シャローコピーとディープコピーの違い

  • シャローコピー(浅いコピー): 参照型フィールドはポインタだけコピーされる。元オブジェクトと複製が同じリストを共有するため、どちらを変更しても両方に影響が出る
  • ディープコピー(深いコピー): 参照型フィールドも新しく作り直す。元オブジェクトと複製は完全に独立している

サンプルコード

PrototypePatternSample.java
import java.util.ArrayList;
import java.util.List;

public class PrototypePatternSample {

    // ❌ アンチパターン: Object.clone() の使用(Java では非推奨)
    static class BadOrderCloneable implements Cloneable {
        String customerId;
        String productId;
        int quantity;

        @Override
        public BadOrderCloneable clone() {
            try {
                return (BadOrderCloneable) super.clone();
                // 問題1: CloneNotSupportedException が発生しうる
                // 問題2: フィールドが参照型の場合、浅いコピーになる(シャローコピー)
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException("複製できません", e);
            }
        }
    }

    // ✅ 推奨: コピーコンストラクタを使った深いコピー(ディープコピー)
    static class Order {
        private final String customerId;
        private final String productId;
        private int quantity;
        private final List<String> notes;

        // 通常コンストラクタ
        Order(String customerId, String productId, int quantity) {
            this.customerId = customerId;
            this.productId = productId;
            this.quantity = quantity;
            this.notes = new ArrayList<>();
        }

        // コピーコンストラクタ(ディープコピー)
        Order(Order other) {
            this.customerId = other.customerId;
            this.productId = other.productId;
            this.quantity = other.quantity;
            this.notes = new ArrayList<>(other.notes); // リストも新規作成
        }

        void addNote(String note) {
            notes.add(note);
        }

        void setQuantity(int quantity) {
            this.quantity = quantity;
        }

        @Override
        public String toString() {
            return "Order{customerId='" + customerId + "', productId='" + productId
                    + "', quantity=" + quantity + ", notes=" + notes + "}";
        }
    }

    public static void main(String[] args) {
        // 雛形注文を作成
        Order template = new Order("C001", "PROD-A", 1);
        template.addNote("通常便");

        System.out.println("雛形: " + template);

        // コピーして別の注文を作成
        Order order1 = new Order(template);
        order1.setQuantity(3);
        order1.addNote("急便希望");

        Order order2 = new Order(template);
        order2.setQuantity(10);

        System.out.println("注文1: " + order1);
        System.out.println("注文2: " + order2);
        System.out.println("雛形(変化なし): " + template);
    }
}

Java 8 ではコピーコンストラクタ(コピー用の別コンストラクタ)を定義するのが最も安全な方法です。Object.clone() は Cloneable の実装が必要なうえ、参照型フィールドのシャローコピー問題があるため推奨されません。

よくあるミス・注意点

⚠️ シャローコピーで List や Map が共有されてしまう

Object.clone() はシャローコピーです。 フィールドに List や Map などの参照型が含まれる場合、コピー元とコピー先が同じオブジェクトを参照します。 コピー先に要素を追加するとコピー元にも反映されてしまいます。 コピーコンストラクタで new ArrayList<>(other.notes) のように 新しいリストを作りましょう。

⚠️ Object.clone() は Java では非推奨

Cloneable インターフェースとObject.clone() の組み合わせは 設計上の欠陥があるとされています(Joshua Bloch 著「Effective Java」でも非推奨とされています)。 Java で Prototype パターンを実装する際は「コピーコンストラクタ」か 「コピーファクトリーメソッド(static な copy() メソッド)」を使うのが推奨されています。

⚠️ record の wither パターンでネストした record を忘れる

Java 17+ の record で withXxx() を定義するとき、フィールドにネストした record や List がある場合は それも新しいインスタンスに差し替える必要があります。 List フィールドには List.copyOf() を使って イミュータブルなコピーを渡しましょう。

テストする観点

  • コピーコンストラクタで作ったオブジェクトが元のオブジェクトと同じフィールド値を持つこと
  • コピー後に元オブジェクトを変更しても、コピー先のフィールド値が変わらないこと(ディープコピーの確認)
  • コピー後にコピー先の notes に要素を追加しても、元オブジェクトの notes が変化しないこと(List の独立性)
  • quantity に 0 を設定してコピーしたとき、コピー先の quantity が 0 であること(境界値)
  • notes が空のオブジェクトをコピーしても、コピー先の notes が空のリストであること(境界値)
  • 雛形から複数回コピーしたとき、それぞれが独立したオブジェクトであること

GitHub でソースコードを見る →