概要
既存のオブジェクトを雛形にして少しだけ値を変えた新しいオブジェクトを作りたい場面は、注文の複製、テストデータの派生作成、テンプレートからの帳票生成など業務でよく出てきます。Java には Object.clone() が用意されていますが、Cloneable インターフェースの設計上の問題(シャローコピーの罠、CloneNotSupportedException の扱い)から、Effective Java でも clone の使用は推奨されていません。この記事では、コピーコンストラクタによるディープコピーと、Java 17 の record + withXxx メソッドによる不変オブジェクトの派生生成という2つのアプローチを示します。clone を避けるべき理由を実例で確認し、実務で安全に複製を行う判断基準を整理します。
使いどころ
注文テンプレートから数量や備考だけ変えた派生注文を複数作成し、一括で登録する
テストデータの基本セットをコピーし、特定のフィールドだけ変更して複数のテストケースに使い回す
帳票テンプレートのレイアウト設定をコピーし、顧客ごとの宛名・金額だけ差し替えて出力する
コード例
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);
}
}Version Coverage
record + withXxx メソッドで不変のまま値を差し替えた新しいインスタンスを生成できる。List.copyOf で防御的コピーも簡潔。
// 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
注意点
Object.clone() はシャローコピーのため、List や Map などの参照型フィールドは元オブジェクトと共有される。片方を変更するともう一方にも影響が出る
clone() を使う場合は Cloneable を implements する必要があるが、CloneNotSupportedException が検査例外のため try-catch が必要になる。設計上の負債になりやすい
record の withXxx メソッドは慣習であり、言語仕様ではない。チーム内で命名規則を統一しておくこと
防御的コピー(defensive copy)を忘れると、コピー元の List を外部から変更されたときにコピー先にも影響する。List.copyOf や new ArrayList<>(original) で切り離す
FAQ
Effective Java でも非推奨とされています。Cloneable の設計上の問題(シャローコピー・検査例外)があるため、コピーコンストラクタか record の withXxx を使うのが安全です。
フィールドが多い場合は Builder パターンと組み合わせ、既存オブジェクトの値で Builder を初期化してから一部だけ上書きする方法が実用的です。
参照型フィールド(List, Map, 可変オブジェクト)を含む場合はディープコピーが必要です。プリミティブ型と不変オブジェクト(String, LocalDate)のみならシャローコピーで十分です。