JavaのListにおける浅いコピーと深いコピーを解説する記事のアイキャッチ。

Java

JavaでListをコピーしても元データが変わる理由|浅いコピーと深いコピー

AIによる要約

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

新人SE
新人SE
new ArrayListでコピーしたのに、コピー側のUser名を変えたら元Listも変わりました。
ポンコツSE
ポンコツSE
List本体は別ですが、中のUserは同じオブジェクトを参照しています。浅いコピーだからです。

画面編集用DTO、取込前のバックアップ、更新前後の比較でListをコピーすることがあります。new ArrayList<>(original)を使えば独立したListになりますが、要素が可変オブジェクトなら完全に独立したデータにはなりません。

この違いを知らずに編集すると、変更前データまで書き換わり、差分監査や取消処理が成立しなくなります。何を独立させたいのかを決め、浅いコピー、不変化、要素コピーを使い分けます。

この記事のポイント

  • new ArrayListはList構造だけをコピーする
  • 要素オブジェクトは同じ参照のまま
  • List.copyOfは変更不可だが深いコピーではない
  • 要素を独立させるなら明示的にコピーする
  • 不変オブジェクトなら浅いコピーでも安全に共有しやすい

List本体と要素は別に考える

Listは要素そのものではなく、要素への参照を順番に保持します。ArrayListのコピーコンストラクタは新しい入れ物を作り、参照を並べ直します。

List&lt;User&gt; original = new ArrayList&lt;&gt;();
original.add(new User("U001", "田中"));

List&lt;User&gt; copied = new ArrayList&lt;&gt;(original);

System.out.println(original == copied);       // false
System.out.println(original.get(0) == copied.get(0)); // true

addや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&lt;User&gt; readOnly = List.copyOf(original);

readOnly.add(new User("U002", "鈴木")); // 例外
readOnly.get(0).setName("佐藤");         // 要素が可変なら実行できる

『不変List』と『不変なデータ構造全体』を混同しません。外部公開APIで変更を防ぎたいなら、要素の不変性も必要です。

要素までコピーする方法

要素を独立させるには、各要素から新しいオブジェクトを作ります。コピーコンストラクタや変換メソッドを用意すると、どの項目をコピーするか明示できます。

List&lt;User&gt; 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&lt;UserView&gt; copied = List.copyOf(originalViews);

ただしrecordの構成要素に可変Listや配列を持てば、その内側は共有されます。不変性はクラス宣言だけでなく、保持する値全体で考えます。

EntityとDTOを同じListで扱わない

JPA EntityのListを画面編集用としてコピーし、Entity自体を変更すると永続化コンテキストの管理対象が変わります。表示・編集用DTOへ変換すると、DB更新との境界を明確にできます。

List&lt;UserEditForm&gt; forms = entities.stream()
        .map(entity -&gt; new UserEditForm(
                entity.getId(), entity.getName()))
        .toList();

更新時はFormから対象Entityへ明示的に値を反映します。自動マッピングを使う場合も、IDや監査項目を上書きしない設定を確認します。

シリアライズコピーを安易に使わない

JSONへ変換して戻す方法は深いコピーに見えますが、型情報、日付形式、無視項目、性能、例外処理へ依存します。コピーのためだけにシリアライズを使うと意図が不透明です。

外部通信のDTO変換が本来の目的なら結果的に独立しますが、ドメインオブジェクトのコピーは明示的なコンストラクタや不変設計を優先します。

JUnitで参照と値を確認する

浅いコピーと深いコピーのテストでは、List本体が別、要素参照が別、値が同じ、コピー側変更が元へ影響しないことを分けて確認します。

@Test
void 要素まで独立してコピーする() {
    List&lt;User&gt; 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の責務に関係します。良いコード/悪いコードの具体例から、変更範囲を限定する設計を学べます。

書籍「改訂新版 良いコード/悪いコードで学ぶ設計入門」の表紙
商品画像:Amazon.co.jp
PR Java新人・若手向け

改訂新版 良いコード/悪いコードで学ぶ設計入門

仙塲 大也 (著)

レビューで直される理由を、Javaの具体例から理解する。

命名、責務、クラス設計、不変性などを良い例と悪い例で比較できます。現場のコードレビューと結びつけやすい一冊です。

  • 設計の悪さを具体例から学びたい
  • レビュー指摘の判断軸を増やしたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

new ArrayListやList.copyOfはList本体を分けたり変更を禁止したりしますが、要素オブジェクトを深くコピーしません。可変要素は元とコピーで同じ参照になります。

編集前データや画面用DTOを独立させるなら、コピーコンストラクタや明示的な変換を使います。不変オブジェクトを選び、List・要素・入れ子のどこまで独立させるかテストしてください。

-Java
-, , , ,