AIによる要約
new ArrayList<>(original)で新しいList本体は作れますが、要素オブジェクトは元Listと同じ参照です。コピー先の要素を変更すると元データからも変更が見えます。これが浅いコピーです。要素まで独立させるには、コピーコンストラクタ、recordなどの不変オブジェクト、明示的な変換を使います。List.copyOfはListの変更を禁止しますが、要素を深くコピーするわけではありません。


画面編集用DTO、取込前のバックアップ、更新前後の比較でListをコピーすることがあります。new ArrayList<>(original)を使えば独立したListになりますが、要素が可変オブジェクトなら完全に独立したデータにはなりません。
この違いを知らずに編集すると、変更前データまで書き換わり、差分監査や取消処理が成立しなくなります。何を独立させたいのかを決め、浅いコピー、不変化、要素コピーを使い分けます。
この記事のポイント
- new ArrayListはList構造だけをコピーする
- 要素オブジェクトは同じ参照のまま
- List.copyOfは変更不可だが深いコピーではない
- 要素を独立させるなら明示的にコピーする
- 不変オブジェクトなら浅いコピーでも安全に共有しやすい
List本体と要素は別に考える
Listは要素そのものではなく、要素への参照を順番に保持します。ArrayListのコピーコンストラクタは新しい入れ物を作り、参照を並べ直します。
List<User> original = new ArrayList<>();
original.add(new User("U001", "田中"));
List<User> copied = new ArrayList<>(original);
System.out.println(original == copied); // false
System.out.println(original.get(0) == copied.get(0)); // trueaddやremoveは別々にできますが、getしたUserは同じです。『Listをコピーした』という言葉だけでは、どこまで独立したか分かりません。
要素を変更すると元からも見える
コピー側から同じUserのsetterを呼ぶと、そのUserオブジェクトが変わります。元Listも同じUserを指すため変更後の値が見えます。
copied.get(0).setName("佐藤");
System.out.println(original.get(0).getName()); // 佐藤これはListの不具合ではなく参照共有の結果です。JPA Entityを含むListなら、setterがダーティチェックでDB更新へつながる可能性もあります。
List.copyOfも深いコピーではない
List.copyOfは変更できないListを返します。addやremoveはできませんが、要素が可変なら要素自体のsetterは呼べます。
List<User> readOnly = List.copyOf(original);
readOnly.add(new User("U002", "鈴木")); // 例外
readOnly.get(0).setName("佐藤"); // 要素が可変なら実行できる『不変List』と『不変なデータ構造全体』を混同しません。外部公開APIで変更を防ぎたいなら、要素の不変性も必要です。
要素までコピーする方法
要素を独立させるには、各要素から新しいオブジェクトを作ります。コピーコンストラクタや変換メソッドを用意すると、どの項目をコピーするか明示できます。
List<User> copied = original.stream()
.map(User::new) // コピーコンストラクタ
.toCollection(ArrayList::new);
class User {
User(User source) {
this.id = source.id;
this.name = source.name;
}
}Userの中にAddressやListがある場合、その内側も独立させる必要があるか判断します。すべてを機械的に深くコピーすると重く、循環参照も問題になるため、用途ごとに境界を決めます。
不変オブジェクトなら共有しやすい
setterを持たず、変更時に新しい値を作るrecordや不変クラスなら、同じ要素を複数Listから参照しても後から書き換わりません。
record UserView(String id, String name) {}
List<UserView> copied = List.copyOf(originalViews);ただしrecordの構成要素に可変Listや配列を持てば、その内側は共有されます。不変性はクラス宣言だけでなく、保持する値全体で考えます。
EntityとDTOを同じListで扱わない
JPA EntityのListを画面編集用としてコピーし、Entity自体を変更すると永続化コンテキストの管理対象が変わります。表示・編集用DTOへ変換すると、DB更新との境界を明確にできます。
List<UserEditForm> forms = entities.stream()
.map(entity -> new UserEditForm(
entity.getId(), entity.getName()))
.toList();更新時はFormから対象Entityへ明示的に値を反映します。自動マッピングを使う場合も、IDや監査項目を上書きしない設定を確認します。
シリアライズコピーを安易に使わない
JSONへ変換して戻す方法は深いコピーに見えますが、型情報、日付形式、無視項目、性能、例外処理へ依存します。コピーのためだけにシリアライズを使うと意図が不透明です。
外部通信のDTO変換が本来の目的なら結果的に独立しますが、ドメインオブジェクトのコピーは明示的なコンストラクタや不変設計を優先します。
JUnitで参照と値を確認する
浅いコピーと深いコピーのテストでは、List本体が別、要素参照が別、値が同じ、コピー側変更が元へ影響しないことを分けて確認します。
@Test
void 要素まで独立してコピーする() {
List<User> copied = copier.copy(original);
assertNotSame(original, copied);
assertNotSame(original.get(0), copied.get(0));
assertEquals(original.get(0).getName(), copied.get(0).getName());
copied.get(0).setName("変更後");
assertNotEquals(original.get(0).getName(), copied.get(0).getName());
}現場レビューでよくある指摘
コピー処理のレビューでは、List構造、要素、入れ子要素のどこまで独立させる必要があるかを確認します。
// レビューコメント例
new ArrayListでList本体は分かれますが、Userは同じ参照です。
編集前スナップショットが必要なら要素もコピーしてください。
// レビューコメント例
List.copyOfはaddを防ぎますが、要素のsetterは防げません。
外部公開するなら要素も不変DTOへ変換してください。
// レビューコメント例
管理中Entityを画面編集用Listへそのまま渡しています。
意図しないダーティチェックを避けるためFormへ変換してください。『deep copyが必要です』だけで終わらず、何を変更する処理か、元データを残す目的は何かを説明します。不要な全オブジェクトコピーは避け、所有境界を明確にします。
提出前のセルフチェック
レビュー前に確認すること
- List本体だけか要素も独立させるか決めたか
- 要素が可変オブジェクトか
- List.copyOfを深いコピーと誤解していないか
- 入れ子のListや配列も共有されていないか
- コピーコンストラクタの項目が十分か
- Entityを画面から直接変更していないか
- 不変DTOやrecordへ変換できないか
- コピー側変更が元へ影響しないテストがあるか
良いコードとデータ設計を学ぶ参考書
コピーの問題は、可変性、所有権、EntityとDTOの責務に関係します。良いコード/悪いコードの具体例から、変更範囲を限定する設計を学べます。
改訂新版 良いコード/悪いコードで学ぶ設計入門
レビューで直される理由を、Javaの具体例から理解する。
命名、責務、クラス設計、不変性などを良い例と悪い例で比較できます。現場のコードレビューと結びつけやすい一冊です。
- 設計の悪さを具体例から学びたい
- レビュー指摘の判断軸を増やしたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
new ArrayListやList.copyOfはList本体を分けたり変更を禁止したりしますが、要素オブジェクトを深くコピーしません。可変要素は元とコピーで同じ参照になります。
編集前データや画面用DTOを独立させるなら、コピーコンストラクタや明示的な変換を使います。不変オブジェクトを選び、List・要素・入れ子のどこまで独立させるかテストしてください。
