はじめに
このまえPG中に遭遇した「ListにDTOをaddしたあと、元のDTOを変更したらListの中身も変わっていた」という事象について整理します。
私を含めた初心者の方のためにも記事を残します。
サンプルコード
Main.java
class UserDto {
    String name;
    UserDto(String name) { this.name = name; }
    public String toString() { return name; }
}
public class Main {
    public static void main(String[] args) {
        List<UserDto> list = new ArrayList<>();
        UserDto dto = new UserDto("Alice");
        list.add(dto);
        System.out.println(list); // [Alice]
        dto.name = "Bob";
        System.out.println(list); // [Bob] ← 変わってる!
    }
}
text
[Alice]
[Bob]
なぜこうなるのか
- JavaのListに格納されるのはオブジェクトそのものではなく参照。
 - list.add(dto)を呼んだ時点で、Listは「dtoが指しているオブジェクトへの参照」を保持。
 - その後dto.name = "Bob"と変更すると、Listが保持している参照先のオブジェクト自体が変わるため、Listの中身も変わったように見える。
 
つまり、addした瞬間に「コピー」されるわけではなく、同じオブジェクトを指しているということです
実務でハマった例
1. DTOを一時的に加工してListに詰めるケース
- バッチ処理で「一時DTO」を作ってListに詰める → 元の変数を触ったら意図せずList側も変わる
 
2. ループで同じインスタンスを使い回すケース
Sample.java
List<UserDto> list = new ArrayList<>();
UserDto dto = new UserDto("Alice");
for (int i = 0; i < 3; i++) {
    dto.name = "User" + i;
    list.add(dto);
}
System.out.println(list); // [User2, User2, User2]
text
[User2, User2, User2]
- ループ内で同じdtoを使い回しているため、Listの中身はすべて同じ参照を指すことになる
 - その結果、最後に代入した値で上書きされてしまう
 
対策方法
様々な対策方法あると思いますが、一例として記載します。
1. 毎回新しいインスタンスを作る
同じオブジェクトを使い回すと、Listの中身が全部同じ参照になってしまいます。
解決策として、addするたびに新しいインスタンスを生成します。
Sample.java
List<UserDto> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
    list.add(new UserDto("User" + i)); // 毎回newする
}
System.out.println(list); // [User0, User1, User2]
2. イミュータブルにしてしまう
「後から書き換えられる」こと自体がバグの原因になるなら、そもそも変更できないようにする。
Sample.java
class UserDto {
    private final String name; // finalで不変にする
    public UserDto(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
3. コピーを作ってからaddする
「元のオブジェクトは残したいけど、似た内容をListに入れたい」場合は、コピーを作ってからaddする。
Sample.java
class UserDto {
    private final String name;
    public UserDto(String name) {
        this.name = name;
    }
    // コピーコンストラクタ
    public UserDto(UserDto other) {
        this.name = other.name;
    }
    public String getName() { return name; }
}
UserDto original = new UserDto("Alice");
List<UserDto> list = new ArrayList<>();
list.add(new UserDto(original)); // コピーをadd
まとめ
- Listに格納されるのは「参照」である
 - そのため、元のオブジェクトを変更するとListの中身も変わる
 - 対策としては
 
- 毎回新しいインスタンスを作る
 - イミュータブルにする
 - コピーを作ってからaddする
 
- どの方法を選ぶかは「処理の目的」と「安全性の優先度」によって決めると良い