Collections.unmodifiableListで元のList変更が反映される理由とList.copyOfとの違いを解説する記事のアイキャッチ。

Java クイズ

JavaのCollections.unmodifiableListで元のList変更が反映される理由|List.copyOfとの違い

AIによる要約

Collections.unmodifiableListは、元のListをコピーせず、変更操作を禁止したビューを返します。そのため、戻り値からaddやremoveをするとUnsupportedOperationExceptionになりますが、元のListへ追加した内容はビューにも反映されます。元のListから切り離した変更不可のスナップショットが必要ならList.copyOf、独立して変更できるListが必要ならnew ArrayListを選びます。ただし、どちらも要素オブジェクトまで複製する深いコピーではありません。

この記事は2026年8月25日にXへ投稿したJavaクイズの詳しい解説です。先に問題を解きたい方は、Xの出題ポストを確認してください。

新人SE
新人SE
変更できないListを作ったのに、元のListへ追加したら中身が変わりました。バグですか?
ポンコツSE
ポンコツSE
仕様どおりです。unmodifiableListはコピーではなく、元のListを参照する変更不可ビューです。

Javaの業務システムでは、Serviceから返す検索結果、キャッシュしたマスタ、DTOが持つ明細などでListを扱います。呼び出し側に勝手に追加・削除されたくないとき、Collections.unmodifiableListを使うことがあります。

ところが、元のListを保持している処理があとから要素を追加すると、変更不可にしたはずのListにも追加内容が見えます。ここで変更不可とコピーは別の機能だと理解していないと、レビューや障害調査で判断を誤ります。

この記事のポイント

  • unmodifiableListはコピーではなく変更不可ビュー
  • ビュー経由のaddやremoveは例外になる
  • 元のListを変更するとビューにも反映される
  • 切り離したスナップショットにはList.copyOfを使う
  • 独立した可変Listにはnew ArrayListを使う
  • 要素オブジェクトは共有されるため深いコピーではない

まずはXのJavaクイズを確認する

Collections.unmodifiableListで作ったListに対して元のArrayListへBを追加したときの出力を問うJavaクイズ画像。
変更不可のListを作ったあと、元のListへBを追加すると何が出るでしょうか。
List<String> base =
    new ArrayList<>(List.of("A"));

List<String> view =
    Collections.unmodifiableList(base);

base.add("B");
System.out.println(view);

答えはBの[A, B]です。例外にはなりません。変更したのはviewではなく、その参照先であるbaseだからです。

答えはBのAとBで、Collections.unmodifiableListはコピーではなく元のListを参照するビューだと説明する画像。
ビューからの変更は禁止されますが、元のListへ加えた変更は見えます。

X上の投稿は、クイズの出題ポスト回答ポストから確認できます。

unmodifiableListはコピーではなくビュー

Collections.unmodifiableList(base)は、baseの全要素を新しいListへコピーするメソッドではありません。元のListを内部に保持し、変更系メソッドを禁止するラッパーを返します。

そのため、読み取り操作は元のListへ委譲されます。getsize、繰り返し処理では、その時点の元Listの内容が見えます。ビューは元のListと別の状態を持っているわけではありません。

List<String> base = new ArrayList<>(List.of("A"));
List<String> view = Collections.unmodifiableList(base);

// ビューからの変更は禁止
assertThrows(UnsupportedOperationException.class,
        () -> view.add("B"));

// 元のListは変更でき、その結果がビューにも見える
base.add("B");
System.out.println(view); // [A, B]

addだけでなく、removesetclearなど、Listの内容を変える操作はビュー側でUnsupportedOperationExceptionになります。ただし、例外が出ることと、元データから切り離されていることは同じではありません。

List.copyOfなら元のListの追加は反映されない

Java 10以降で、作成時点の内容を保持する変更不可Listが必要ならList.copyOfが候補です。元のListへあとから要素を追加しても、作成済みのコピーへは反映されません。

List<String> base = new ArrayList<>(List.of("A"));
List<String> snapshot = List.copyOf(base);

base.add("B");

System.out.println(base);     // [A, B]
System.out.println(snapshot); // [A]

snapshot.add("C")UnsupportedOperationExceptionになります。つまりList.copyOfは、元Listの構造変更から切り離しつつ、コピー側からの変更も禁止します。

Java 8案件で同じ意図を表すなら、Collections.unmodifiableList(new ArrayList<>(base))のように、先にコピーしてから変更不可ビューで包みます。コードが長くなるため、コメントではなく変数名や共通メソッドで意図を明確にします。

new ArrayListは独立して変更できるコピー

コピー先でも追加・削除したいなら、new ArrayList<>(base)を使います。元Listとコピー先の要素数は独立します。

List<String> base = new ArrayList<>(List.of("A"));
List<String> editableCopy = new ArrayList<>(base);

base.add("B");
editableCopy.add("C");

System.out.println(base);         // [A, B]
System.out.println(editableCopy); // [A, C]

ここまでを判断基準にすると、選択肢は次の3つです。

  • 元の最新状態を読み取り専用で見せる: Collections.unmodifiableList
  • 作成時点の状態を変更不可で渡す: List.copyOf
  • 別のListとして自由に変更する: new ArrayList

「とりあえず変更不可にする」ではなく、相手へ最新状態を見せたいのか、呼び出した瞬間のスナップショットを渡したいのかを先に決めます。

Listをコピーしても要素は共有される

List.copyOfnew ArrayListが複製するのはListの入れ物です。中に入っている要素オブジェクトまで自動的に複製するわけではありません。これは浅いコピーです。

User user = new User("before");
List<User> base = new ArrayList<>(List.of(user));
List<User> snapshot = List.copyOf(base);

user.setName("after");

System.out.println(base.get(0).getName());     // after
System.out.println(snapshot.get(0).getName()); // after

要素まで変更させたくないなら、要素をrecordや不変クラスにする、DTOへ変換する、必要な項目だけ複製するといった設計が必要です。Listだけを変更不可にしても、要素のsetterまでは禁止できません。

nullを含む場合は動きが違う

List.copyOfnull要素を許可しません。元Listにnullが含まれていると、コピー作成時にNullPointerExceptionになります。一方、unmodifiableListは元Listを包むため、元Listが保持しているnullもそのまま見えます。

List<String> base = new ArrayList<>();
base.add(null);

List<String> view = Collections.unmodifiableList(base);
System.out.println(view); // [null]

List.copyOf(base); // NullPointerException

現場では、nullを残したいからunmodifiableListを選ぶのではなく、そもそもコレクション内のnullを許可する仕様か確認します。検索結果なしをnull要素で表すと、利用側の分岐が増えやすいためです。

API境界では防御的コピーを検討する

コンストラクタで受け取ったListをそのままフィールドへ保存すると、呼び出し側が元Listを変更しただけでクラス内部の状態まで変わります。意図しない外部変更を防ぐ方法が防御的コピーです。

final class OrderBatch {
    private final List<OrderLine> lines;

    OrderBatch(List<OrderLine> source) {
        this.lines = List.copyOf(source);
    }

    List<OrderLine> getLines() {
        return lines;
    }
}

この例では、コンストラクタ呼び出し後にsource.add(...)されても、OrderBatchの要素数は変わりません。さらにフィールド自体が変更不可なので、getterから返してもaddやremoveはできません。

ただし、OrderLineが可変クラスなら、そのフィールド変更は共有されます。クラス全体を変更不可として扱うなら、要素の設計まで確認してください。

内部で更新するListをgetterから返す場合

Serviceや集約が内部でListを更新し続ける場合、getterから何を返すかで契約が変わります。

  • 現在の内部状態を常に見せたいなら、変更不可ビューを返す
  • 呼び出し時点の状態を固定して渡したいなら、List.copyOfを返す
  • 大量データで毎回コピーするコストが問題なら、APIの利用頻度と所有権を設計し直す

unmodifiableListを返してもスレッドセーフにはなりません。別スレッドが元Listを変更しながら繰り返し処理をすると、内容の不整合やConcurrentModificationExceptionにつながる可能性があります。変更禁止と排他制御を混同しないでください。

JUnitで契約をテストする

標準APIの仕様をテストすることが目的ではありません。自作メソッドが「ライブビューを返す」のか「スナップショットを返す」のか、チームで決めた契約をテストします。

@Test
void 変更不可ビューには元Listの追加が見える() {
    List<String> base = new ArrayList<>(List.of("A"));
    List<String> actual = Collections.unmodifiableList(base);

    base.add("B");

    assertEquals(List.of("A", "B"), actual);
    assertThrows(UnsupportedOperationException.class,
            () -> actual.add("C"));
}

@Test
void copyOfは元Listの追加から切り離される() {
    List<String> base = new ArrayList<>(List.of("A"));
    List<String> actual = List.copyOf(base);

    base.add("B");

    assertEquals(List.of("A"), actual);
}

メソッド名に「変更不可Listを返す」とだけ書くより、元データ更新の反映有無をテスト名と期待値で示す方が、保守担当者へ契約が伝わります。

現場レビューでよくある指摘

レビューでは、変更系メソッドが呼べないことだけでなく、元Listの所有者、更新タイミング、要素の可変性、nullの扱いを確認します。

// レビューコメント例
unmodifiableListは元のListを参照するビューです。
作成時点のスナップショットが要件ならList.copyOfを検討してください。

// レビューコメント例
Listはコピーされていますが、要素のOrderLineは共有されています。
要素の変更も禁止したい仕様か確認してください。

// レビューコメント例
このListにはnullが入る可能性があります。
List.copyOfへ変更する前に、nullを許可する入力仕様か整理してください。

// レビューコメント例
変更不可ビューはスレッドセーフを保証しません。
別スレッドから元Listが更新される設計なら排他と公開方法を確認してください。

若手がレビューで「List.copyOfへ直してください」と言われた場合、単なる書き方の好みではありません。呼び出し元の変更から内部状態を守る、防御的コピーの意図があるかを確認します。

提出前のセルフチェック

レビュー前に確認すること

  • 元のListの更新を相手へ見せる仕様か
  • 作成時点のスナップショットが必要か
  • コピー先で追加・削除する必要があるか
  • ビューからの変更で例外になることを確認したか
  • 要素オブジェクトが可変ではないか
  • null要素を許可する仕様か
  • API境界で防御的コピーが必要か
  • 別スレッドから元Listが変更されないか
  • テストで元List変更後の期待結果を明示したか

Javaのコレクションを実践的に学ぶ参考書

Listの生成方法、例外、ジェネリクス、コレクションAPIを体系的に整理すると、「動かないように包む」のか「状態を切り離す」のかを判断しやすくなります。基本文法を終えた後の復習には実践編が向いています。

書籍「スッキリわかるJava入門 実践編 第5版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

スッキリわかるJava入門 実践編 第5版

中山 清喬 (著), 株式会社フレアリンク (監修)

基礎文法の次に必要な、現場寄りのJava知識を補う。

コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。

  • Java基礎の次に何を学ぶか迷っている
  • コレクションやStreamを整理したい

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

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

まとめ

Collections.unmodifiableListはコピーではなく、元のListを参照する変更不可ビューです。ビューからのaddやremoveは例外になりますが、元のListへ加えた変更はビューにも反映されます。

元データから切り離した変更不可スナップショットにはList.copyOf、独立して変更するコピーにはnew ArrayListを使います。さらに、Listをコピーしても要素は共有されるため、要素の可変性、null、スレッド、APIの所有権まで含めて選んでください。

-Java, クイズ
-, , ,