概要

既存のオブジェクトを雛形にして少しだけ値を変えた新しいオブジェクトを作りたい場面は、注文の複製、テストデータの派生作成、テンプレートからの帳票生成など業務でよく出てきます。Java には Object.clone() が用意されていますが、Cloneable インターフェースの設計上の問題(シャローコピーの罠、CloneNotSupportedException の扱い)から、Effective Java でも clone の使用は推奨されていません。この記事では、コピーコンストラクタによるディープコピーと、Java 17 の record + withXxx メソッドによる不変オブジェクトの派生生成という2つのアプローチを示します。clone を避けるべき理由を実例で確認し、実務で安全に複製を行う判断基準を整理します。

使いどころ

注文テンプレートから数量や備考だけ変えた派生注文を複数作成し、一括で登録する

テストデータの基本セットをコピーし、特定のフィールドだけ変更して複数のテストケースに使い回す

帳票テンプレートのレイアウト設定をコピーし、顧客ごとの宛名・金額だけ差し替えて出力する

コード例

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

public class PrototypePatternSample {

    // Java 17: record + withXxx でイミュータブルな複製
    record Order(String customerId, String productId,
                 int quantity, List<String> notes) {

        // コンパクトコンストラクタ: notes を防御的にコピー
        Order {
            notes = List.copyOf(notes);
        }

        Order withQuantity(int newQuantity) {
            return new Order(customerId, productId, newQuantity, notes);
        }

        Order withNote(String additionalNote) {
            var newNotes = new ArrayList<>(notes);
            newNotes.add(additionalNote);
            return new Order(customerId, productId, quantity, newNotes);
        }
    }

    public static void main(String[] args) {
        // 雛形注文を作成
        var template = new Order("C001", "PROD-A", 1, List.of("通常便"));
        System.out.println("雛形: " + template);

        // withXxx で値を変えた新しいインスタンスを生成
        var order1 = template.withQuantity(3).withNote("急便希望");
        var order2 = template.withQuantity(10);

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

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record + withXxx メソッドで不変のまま値を差し替えた新しいインスタンスを生成できる。List.copyOf で防御的コピーも簡潔。

Java 17
// Java 17: record + withXxx で不変コピー
record Order(String customerId, String productId,
             int quantity, List<String> notes) {
    Order withQuantity(int q) {
        return new Order(customerId, productId, q, notes);
    }
}
var copy = template.withQuantity(10);

Library Comparison

標準 API(コピーコンストラクタ / record)複製対象のフィールド構成が明確で、防御的コピーを自前で制御したいとき。フィールドが多いとコピーコンストラクタの記述量が増える。
Apache Commons Lang(SerializationUtils)Serializable なオブジェクトを丸ごとディープコピーしたいとき。シリアライズ経由のため性能は低い。Serializable でないオブジェクトには使えない。
MapStructDTO 間の変換とコピーを型安全に自動生成したいとき。アノテーションプロセッサが必要。単純な複製だけなら過剰。

注意点

Object.clone() はシャローコピーのため、List や Map などの参照型フィールドは元オブジェクトと共有される。片方を変更するともう一方にも影響が出る

clone() を使う場合は Cloneable を implements する必要があるが、CloneNotSupportedException が検査例外のため try-catch が必要になる。設計上の負債になりやすい

record の withXxx メソッドは慣習であり、言語仕様ではない。チーム内で命名規則を統一しておくこと

防御的コピー(defensive copy)を忘れると、コピー元の List を外部から変更されたときにコピー先にも影響する。List.copyOf や new ArrayList<>(original) で切り離す

FAQ

Object.clone() は本当に使うべきではないですか。

Effective Java でも非推奨とされています。Cloneable の設計上の問題(シャローコピー・検査例外)があるため、コピーコンストラクタか record の withXxx を使うのが安全です。

record で withXxx を書くのが面倒な場合の代替手段はありますか。

フィールドが多い場合は Builder パターンと組み合わせ、既存オブジェクトの値で Builder を初期化してから一部だけ上書きする方法が実用的です。

シャローコピーとディープコピーの判断基準は何ですか。

参照型フィールド(List, Map, 可変オブジェクト)を含む場合はディープコピーが必要です。プリミティブ型と不変オブジェクト(String, LocalDate)のみならシャローコピーで十分です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます