java-recipes

ホーム コピーパターン › Cop-02

Cop-02: よくある落とし穴と対策

コピー操作で初心者がはまりやすいバグのパターンを整理します。 参照の共有・浅いコピーの罠・unmodifiableList の誤解など、 実際のコードで確認しながら正しい対策を学びます。

代表的な落とし穴の種類

Java のコピー操作には、見た目はコピーしているように見えても実際には同じオブジェクトを参照し続けているケースが多くあります。 特にリストやオブジェクトを含む複合的なデータ構造では、コピーの深さ(浅いコピー・深いコピー)を意識しないとバグの原因になります。

主な落とし穴

  • 参照コピー:List<String> copy = original; は コピーではなく同じリストへの参照です。どちらを変更しても同じリストが変わります。
  • 浅いコピーの罠:new ArrayList<>(original) はリスト自体は新しく作りますが、 中の要素オブジェクトは共有されています。要素のフィールドを変更すると元のリストにも影響します。
  • unmodifiableList の誤解:Collections.unmodifiableList() は変更不可のラッパーを作るだけです。 元のリストを変更すると、ラッパーを通じて取得しても変更後の値が見えてしまいます。

サンプルコード

CopyPitfallSample.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CopyPitfallSample {

    static class Person {
        String name;
        List<String> hobbies;

        Person(String name, List<String> hobbies) {
            this.name = name;
            this.hobbies = hobbies;
        }

        @Override
        public String toString() {
            return "Person{name='" + name + "', hobbies=" + hobbies + "}";
        }
    }

    public static void main(String[] args) {
        // ❌ 落とし穴 1: 参照のコピー(同じオブジェクトを指す)
        System.out.println("=== 落とし穴 1: 参照コピー ===");
        List<String> original = new ArrayList<>();
        original.add("Java");
        original.add("Python");
        List<String> ref = original; // 同じリストを参照
        ref.add("Kotlin");
        System.out.println("original: " + original); // Kotlin も入っている!
        System.out.println("ref:      " + ref);

        // ⚠️ 落とし穴 2: 浅いコピー(要素は共有)
        System.out.println("\n=== 落とし穴 2: 浅いコピー ===");
        List<Person> people = new ArrayList<>();
        people.add(new Person("田中", new ArrayList<>(Collections.singletonList("サッカー"))));
        List<Person> shallowCopy = new ArrayList<>(people); // 浅いコピー
        shallowCopy.get(0).hobbies.add("テニス"); // 要素は共有されているので元も変わる
        System.out.println("original[0]: " + people.get(0));     // テニスが入っている!
        System.out.println("copy[0]:     " + shallowCopy.get(0));

        // ⚠️ 落とし穴 3: unmodifiableList は元リストの変更を反映
        System.out.println("\n=== 落とし穴 3: unmodifiableList の誤解 ===");
        List<String> mutable = new ArrayList<>();
        mutable.add("A");
        mutable.add("B");
        List<String> unmodifiable = Collections.unmodifiableList(mutable);
        mutable.add("C"); // 元リストを変更
        System.out.println("unmodifiable: " + unmodifiable); // C が入っている!
        // unmodifiable.add("D"); // → UnsupportedOperationException

        // ✅ 対策: ディープコピーを自分で実装
        System.out.println("\n=== ✅ 対策: ディープコピー ===");
        List<Person> deepCopy = new ArrayList<>();
        for (Person p : people) {
            deepCopy.add(new Person(p.name, new ArrayList<>(p.hobbies)));
        }
        deepCopy.get(0).hobbies.add("野球");
        System.out.println("original[0]: " + people.get(0));  // 変わらない
        System.out.println("deepCopy[0]: " + deepCopy.get(0));
    }
}

Java 8 では Collections.unmodifiableList() はあくまでラッパーです。元のリストを変更すると unmodifiableList にも影響が出ます。完全に独立したリストが必要なときは new ArrayList<>() でコピーし、要素も含めてディープコピーを行います。

よくあるミス・注意点

⚠️ ネストしたオブジェクトは浅いコピーだけでは不十分

PersonList<String> hobbies を持っているとき、new Person(p.name, p.hobbies) のようにコピーするとhobbies リストは共有されたままです。new ArrayList<>(p.hobbies) のように明示的にコピーする必要があります。

⚠️ List.of() で作ったリストを変更しようとすると例外になる

Java 9+ の List.of() と Java 10+ の List.copyOf() は不変リストを返します。add()remove() を呼び出すとUnsupportedOperationException がスローされます。 後から変更する可能性があるリストは new ArrayList<>(List.of(...)) のように 可変リストに変換してから使いましょう。

⚠️ Collections.unmodifiableList() は「変更できない」ではなく「変更させない」

Collections.unmodifiableList(mutable) は そのラッパー自体への追加・削除を禁止するだけです。元のmutable リストを変更すると、 ラッパーを通して参照しても変更後の内容が見えます。 完全に独立したスナップショットが必要な場合はList.copyOf(mutable)(Java 10+)を使いましょう。

テストする観点

  • 参照コピー後に片方のリストを変更したとき、もう一方にも反映されること
  • 浅いコピー後にコピー先の要素のフィールドを変更したとき、元のリストの要素も変わること
  • ディープコピー後に要素を変更しても、元のリストの要素が変わらないこと
  • Collections.unmodifiableList() でラップしたリストにadd() を呼び出したときUnsupportedOperationException がスローされること
  • 元のリストを変更したとき、unmodifiableList() を通じた参照にも変更が反映されること(境界値)
  • 空のリスト(要素数 0)でディープコピーしても例外が発生しないこと(境界値)

GitHub でソースコードを見る →