List.of と Arrays.asList の違いは、List.of が「要素の追加・削除・変更がすべてできず null も入らない不変リスト」、Arrays.asList が「要素の変更(set)はできるが追加・削除はできず、元の配列と中身がつながった固定長リスト」である点です。 どちらも add を呼ぶと UnsupportedOperationException になります。

この記事では、この2つに List.copyOf・Collections.unmodifiableList・Stream.toList・Collectors.toList・new ArrayList を加えた 7種類のリストを、add/remove/set/null/元配列・元リストとの連動まで ○× の比較表 にまとめました。表の ○× はすべて OpenJDK 21.0.10 で実行して確認 したもので、UnsupportedOperationException のスタックトレース全文、Arrays.asList(int[]) が List<int[]> になる罠、Set.of/Map.of の重複エラーも実行結果つきで紹介します。

List・Set・Map の使い分けそのものは既存記事「Java List Set Map 使い分け|選び方フローと実測つき」で扱っています。本記事は リストの作り方による「変更できる/できない」の違い に絞ります。

List.of・Arrays.asList など7種の早見表

迷ったら「あとで要素を増やす → new ArrayList<>(...)」「変えない定数 → List.of」「受け取ったリストを固定して保持 → List.copyOf」 と覚えると大半の場面で困りません。

作り方 add / remove set null要素 contains(null) 元配列・元リストの変更が反映 追加された版
List.of(...) × × ×(NPE) ×(NPE) 反映しない(コピー) Java 9
Arrays.asList(...) × ○ ○ ○(false) 反映する(配列と共有) Java 1.2
List.copyOf(coll) × × ×(NPE) ×(NPE) 反映しない(コピー) Java 10
Collections.unmodifiableList(list) × × ○ ○(false) 反映する(ビュー) Java 1.2
stream.toList() × × ○ ○(false) 反映しない Java 16
collect(Collectors.toList()) ○(※) ○(※) ○ ○(false) 反映しない Java 8
new ArrayList<>(...) ○ ○ ○ ○(false) 反映しない(コピー) Java 1.2

※ Collectors.toList() は Java 21 の実行では java.util.ArrayList が返り変更できましたが、Javadoc では「返す List の型・可変性は保証しない」とされています。変更する前提なら Collectors.toCollection(ArrayList::new) を使うと意図が明確です。

やりたいこと おすすめ
定数や固定の選択肢を持つ List.of("A", "B")
あとで add / remove する new ArrayList<>(List.of(...))
配列をリストとして扱い、配列も一緒に書き換えたい Arrays.asList(arr)
引数で受け取ったリストを、呼び出し元の変更から守って保持 List.copyOf(list)
内部の可変リストを、読み取り専用で外に見せる Collections.unmodifiableList(list)
Stream の結果を変更しない stream.toList()
Stream の結果を変更する collect(Collectors.toCollection(ArrayList::new))

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java List.ofとArrays.asListの違い|変更可否の比較表」

7種類のリストを実行して比べる

早見表の add / remove / set / null の列は、次のコードで確認しました。それぞれの作り方でリストを作り、操作が成功すれば「○」、例外なら「×(例外名)」を出します。

// Compare.java(実行: java Compare.java)
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Compare {
    static String t(Runnable r) {
        try { r.run(); return "○"; }
        catch (Exception e) { return "×(" + e.getClass().getSimpleName() + ")"; }
    }
    static String tb(Supplier<Boolean> s) {
        try { return "○(" + s.get() + ")"; }
        catch (Exception e) { return "×(" + e.getClass().getSimpleName() + ")"; }
    }
    public static void main(String[] args) {
        Map<String, Supplier<List<String>>> makers = new LinkedHashMap<>();
        makers.put("List.of", () -> List.of("a", "b", "c"));
        makers.put("Arrays.asList", () -> Arrays.asList("a", "b", "c"));
        makers.put("List.copyOf", () -> List.copyOf(new ArrayList<>(List.of("a", "b", "c"))));
        makers.put("Collections.unmodifiableList", () -> Collections.unmodifiableList(new ArrayList<>(List.of("a", "b", "c"))));
        makers.put("Stream.toList", () -> Stream.of("a", "b", "c").toList());
        makers.put("Collectors.toList", () -> Stream.of("a", "b", "c").collect(Collectors.toList()));
        makers.put("new ArrayList", () -> new ArrayList<>(List.of("a", "b", "c")));

        System.out.println("種類 | クラス | add | remove | set | sort | null要素で作成 | contains(null)");
        for (var e : makers.entrySet()) {
            String name = e.getKey();
            List<String> cls = e.getValue().get();
            String add = t(() -> e.getValue().get().add("d"));
            String remove = t(() -> e.getValue().get().remove(0));
            String set = t(() -> e.getValue().get().set(0, "z"));
            String sort = t(() -> e.getValue().get().sort(Comparator.reverseOrder()));
            String nul = switch (name) {
                case "List.of" -> t(() -> List.of("a", null));
                case "Arrays.asList" -> t(() -> Arrays.asList("a", null));
                case "List.copyOf" -> t(() -> List.copyOf(Arrays.asList("a", null)));
                case "Collections.unmodifiableList" -> t(() -> Collections.unmodifiableList(Arrays.asList("a", null)));
                case "Stream.toList" -> t(() -> Stream.of("a", null).toList());
                case "Collectors.toList" -> t(() -> Stream.of("a", null).collect(Collectors.toList()));
                default -> t(() -> new ArrayList<>(Arrays.asList("a", null)));
            };
            String cn = tb(() -> e.getValue().get().contains(null));
            System.out.println(name + " | " + cls.getClass().getName() + " | " + add + " | " + remove + " | " + set + " | " + sort + " | " + nul + " | " + cn);
        }
    }
}

実行結果(OpenJDK 21.0.10):

種類 | クラス | add | remove | set | sort | null要素で作成 | contains(null)
List.of | java.util.ImmutableCollections$ListN | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(NullPointerException) | ×(NullPointerException)
Arrays.asList | java.util.Arrays$ArrayList | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ○ | ○ | ○ | ○(false)
List.copyOf | java.util.ImmutableCollections$ListN | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(NullPointerException) | ×(NullPointerException)
Collections.unmodifiableList | java.util.Collections$UnmodifiableRandomAccessList | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ○ | ○(false)
Stream.toList | java.util.ImmutableCollections$ListN | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ×(UnsupportedOperationException) | ○ | ○(false)
Collectors.toList | java.util.ArrayList | ○ | ○ | ○ | ○ | ○ | ○(false)
new ArrayList | java.util.ArrayList | ○ | ○ | ○ | ○ | ○ | ○(false)

結果から読み取れるポイントです。

  • Arrays.asList は set と sort ができます。 要素の「入れ替え」はできても、「数(長さ)を変える」add/remove はできません。中身は配列なので長さが固定、と考えるとわかりやすくなります。
  • List.of/List.copyOf は contains(null) でも NullPointerException になります。 null を「探す」だけでも例外になるのが意外な点です。indexOf(null) も同じく NPE でした(下の Linked.java で確認)。
  • Stream.toList() は不変ですが null 要素を許可します。 List.of と同じ内部クラス(ImmutableCollections$ListN)でも、null の扱いが違います。
  • Collections.unmodifiableList は内部クラス名が Unmodifiable... の「ラッパー」です。中身は元のリストそのものです(後述)。

なぜ List.of は null を拒否するのか

List.of などの不変コレクションは、Javadoc で「null 要素を許可しない」と定められています。 作成時の NullPointerException に加えて、contains(null)/indexOf(null) でも NPE になります。null を「値がない」意味で使いたいときは、リストに入れるのではなく Optional や空リストで表すと扱いやすくなります(Optional は既存記事「Java Optionalの使い方」を参照)。

UnsupportedOperationException の原因と直し方

UnsupportedOperationException は「このリストはその操作に対応していません」という実行時例外です。 コンパイルは通るため、実行して初めて気づきます。

List.of に add したときのスタックトレース全文

// Trace.java
import java.util.*;

public class Trace {
    public static void main(String[] args) {
        List<String> items = List.of("ペン", "ノート");
        items.add("消しゴム");
    }
}

実行結果(OpenJDK 21.0.10):

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
    at Trace.main(Trace.java:7)

Arrays.asList に add したときのスタックトレース全文

// Trace2.java
import java.util.*;

public class Trace2 {
    public static void main(String[] args) {
        List<String> items = Arrays.asList("ペン", "ノート");
        items.add("消しゴム");
    }
}

実行結果(OpenJDK 21.0.10):

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.AbstractList.add(AbstractList.java:155)
    at java.base/java.util.AbstractList.add(AbstractList.java:113)
    at Trace2.main(Trace2.java:7)

スタックトレースの上の行を見ると、どの作り方のリストかがわかります。

トレースに出るクラス 作り方 例外メッセージ
ImmutableCollections.uoe List.of/List.copyOf/Stream.toList なし(null)
AbstractList.add/AbstractList.remove Arrays.asList なし(null)
Collections$UnmodifiableCollection.add Collections.unmodifiableList なし(null)

(3行目は Collections.unmodifiableList(new ArrayList<>(List.of("ペン"))) に add して確認した結果で、トレースの先頭は at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1092) でした。)

例外メッセージが空なので、e.getMessage() だけをログに出すと原因がわかりません。 ログには例外オブジェクトごと(スタックトレースつき)出すと、上の表のように作り方を特定できます。自分のコード(Trace.main(Trace.java:7))の行番号から、どの変数への操作かを確認します。

直し方:new ArrayList<>(...) で可変リストにする

要素を増減したいなら、new ArrayList<>(元のリスト) で可変のコピーを作ります。

// Fix.java
import java.util.*;

public class Fix {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>(List.of("ペン", "ノート"));
        items.add("消しゴム");
        items.remove("ペン");
        System.out.println(items);
        List<String> fromArray = new ArrayList<>(Arrays.asList("A", "B"));
        fromArray.add("C");
        System.out.println(fromArray);
    }
}

実行結果(OpenJDK 21.0.10):

[ノート, 消しゴム]
[A, B, C]
状況 直す前 直した後
初期値つきで後から追加 List.of("a") new ArrayList<>(List.of("a"))
配列から作って後から追加 Arrays.asList(arr) new ArrayList<>(Arrays.asList(arr))
Stream の結果に追加 stream.toList() stream.collect(Collectors.toCollection(ArrayList::new))
受け取ったリストに追加(呼び出し元が不変かも) param.add(x) var copy = new ArrayList<>(param); copy.add(x);
メソッドの戻り値に追加 戻り値をそのまま変更 戻り値を変更しない設計にするか、コピーしてから変更

「受け取ったリストを直接変更しない」ことを習慣にすると、相手がどの作り方でリストを渡してきても安全です。

元配列・元リストとの連動を確認する

Arrays.asList は元の配列と、Collections.unmodifiableList は元のリストと中身を共有します。 一方 List.of・List.copyOf・Stream.toList・new ArrayList はコピーなので連動しません。

// Linked.java
import java.util.*;

public class Linked {
    public static void main(String[] args) {
        // 元配列との連動
        String[] arr = {"a", "b", "c"};
        List<String> asList = Arrays.asList(arr);
        List<String> of = List.of(arr);
        arr[0] = "X";
        System.out.println("配列を変更 -> asList=" + asList + " / List.of=" + of);
        asList.set(1, "Y");
        System.out.println("asList.set -> 配列=" + Arrays.toString(arr));

        // 元リストとの連動
        List<String> src = new ArrayList<>(List.of("a", "b"));
        List<String> unmod = Collections.unmodifiableList(src);
        List<String> copy = List.copyOf(src);
        List<String> arrayCopy = new ArrayList<>(src);
        List<String> streamed = src.stream().toList();
        src.add("c");
        src.set(0, "A");
        System.out.println("元リストを変更 -> src=" + src);
        System.out.println("  unmodifiableList=" + unmod);
        System.out.println("  List.copyOf     =" + copy);
        System.out.println("  new ArrayList   =" + arrayCopy);
        System.out.println("  Stream.toList   =" + streamed);

        // copyOf は不変リストならコピーしない
        List<String> immutable = List.of("a");
        System.out.println("List.copyOf(List.of) 同一インスタンス? " + (List.copyOf(immutable) == immutable));
        System.out.println("List.copyOf(unmodifiableList) 同一インスタンス? " + (List.copyOf(unmod) == unmod));
        System.out.println("unmodifiableList(unmodifiableList) 同一インスタンス? " + (Collections.unmodifiableList(unmod) == unmod));

        // indexOf(null)
        try { List.of("a").indexOf(null); System.out.println("List.of.indexOf(null) OK"); }
        catch (Exception e) { System.out.println("List.of.indexOf(null) -> " + e); }
        System.out.println("Arrays.asList.indexOf(null) -> " + Arrays.asList("a").indexOf(null));

        // equals
        System.out.println("List.of(a,b).equals(Arrays.asList(a,b)) = " + List.of("a","b").equals(Arrays.asList("a","b")));
        System.out.println("List.of(a,b).equals(new ArrayList) = " + List.of("a","b").equals(new ArrayList<>(List.of("a","b"))));
    }
}

実行結果(OpenJDK 21.0.10):

配列を変更 -> asList=[X, b, c] / List.of=[a, b, c]
asList.set -> 配列=[X, Y, c]
元リストを変更 -> src=[A, b, c]
  unmodifiableList=[A, b, c]
  List.copyOf     =[a, b]
  new ArrayList   =[a, b]
  Stream.toList   =[a, b]
List.copyOf(List.of) 同一インスタンス? true
List.copyOf(unmodifiableList) 同一インスタンス? false
unmodifiableList(unmodifiableList) 同一インスタンス? true
List.of.indexOf(null) -> java.lang.NullPointerException
Arrays.asList.indexOf(null) -> -1
List.of(a,b).equals(Arrays.asList(a,b)) = true
List.of(a,b).equals(new ArrayList) = true

Arrays.asList は配列と双方向につながる

配列を書き換えるとリストに、リストを set すると配列に反映されました。 Arrays.asList は配列をコピーせず、配列を「リストの形で見る」窓のようなものだからです。配列を別の用途で書き換える予定があるなら、List.of(arr) か new ArrayList<>(Arrays.asList(arr)) でコピーしておきます。

unmodifiableList は「読み取り専用のビュー」

Collections.unmodifiableList が防ぐのは「このラッパー経由の変更」だけです。 元の src に add/set すると、unmod から見える内容も [A, b, c] に変わりました。不変(中身が二度と変わらない)ではなく、読み取り専用の窓と考えます。

flowchart LR
    SRC["src(ArrayList)<br>add・set できる"] --> UNMOD["unmodifiableList(src)<br>読み取り専用の窓"]
    SRC -. コピー .-> COPY["List.copyOf(src)<br>独立した不変リスト"]
    UNMOD -->|"add / set"| UOE["UnsupportedOperationException"]
    SRC -->|"add / set"| UNMOD
目的 選ぶもの 理由
クラス内部で更新し続けるリストを、getter で読み取り専用で返す Collections.unmodifiableList コピーせず軽い。最新の状態が見える
ある時点の内容を固定して保持(コンストラクタで受け取る等) List.copyOf 元が変わっても影響しない
返した後の状態変化を見せたくない List.copyOf スナップショットになる

List.copyOf はコピーを省略することがある

List.copyOf(List.of(...)) は同じインスタンスを返しました(true)。 元がすでに不変リストならコピーは不要だからです。一方、unmodifiableList は中身が変わりうるため、List.copyOf は新しく作りました(false)。どちらの場合も「受け取った後に中身が変わらない」ことは同じなので、防御的コピーには List.copyOf を使えば、余分なコピーも自動で避けられます。

equals は作り方に関係なく中身で比べる

List.of(a,b).equals(Arrays.asList(a,b)) は true でした。 List の equals は「同じ順で同じ要素か」で判定する決まりなので、作り方が違っても中身が同じなら等しくなります。テストで比較するときに作り方を揃える必要はありません。

Arrays.asList(int[]) が List になる罠

Arrays.asList に int[] のようなプリミティブ型の配列を渡すと、「int[] を1個だけ持つリスト」になります。 可変長引数 T... の T にプリミティブ型は入れないため、配列全体が1つの要素として扱われるからです。

// IntArr.java
import java.util.*;
import java.util.stream.*;

public class IntArr {
    public static void main(String[] args) {
        int[] nums = {3, 1, 2};
        List<int[]> wrong = Arrays.asList(nums);
        System.out.println("Arrays.asList(int[]).size() = " + wrong.size());
        System.out.println("中身 = " + wrong.get(0).getClass().getSimpleName() + " " + Arrays.toString(wrong.get(0)));
        System.out.println("contains(3)? " + Arrays.asList(nums).contains(3));
        List<int[]> wrong2 = List.of(nums);
        System.out.println("List.of(int[]).size() = " + wrong2.size());

        Integer[] boxed = {3, 1, 2};
        System.out.println("Arrays.asList(Integer[]) = " + Arrays.asList(boxed) + " size=" + Arrays.asList(boxed).size());
        List<Integer> ok1 = Arrays.stream(nums).boxed().toList();
        List<Integer> ok2 = IntStream.of(nums).boxed().collect(Collectors.toCollection(ArrayList::new));
        System.out.println("Arrays.stream(int[]).boxed().toList() = " + ok1 + " contains(3)? " + ok1.contains(3));
        System.out.println("可変版 = " + ok2);
    }
}

実行結果(OpenJDK 21.0.10):

Arrays.asList(int[]).size() = 1
中身 = int[] [3, 1, 2]
contains(3)? false
List.of(int[]).size() = 1
Arrays.asList(Integer[]) = [3, 1, 2] size=3
Arrays.stream(int[]).boxed().toList() = [3, 1, 2] contains(3)? true
可変版 = [3, 1, 2]

size() が 1、contains(3) が false になりました。var や List<int[]> で受けるとコンパイルが通るため気づきにくい点です。List.of(int[]) も同じく要素数1です。

受け取る型を List<Integer> と明示しておくと、コンパイル時に気づけます。 次は List<Integer> list = Arrays.asList(nums); と書いて java Bad.java を実行したときのエラーです(OpenJDK 21.0.10)。

Bad.java:5: error: incompatible types: inference variable T has incompatible bounds
        List<Integer> list = Arrays.asList(nums);
                                          ^
    equality constraints: Integer
    lower bounds: int[]
  where T is a type-variable:
    T extends Object declared in method <T>asList(T...)
1 error
error: compilation failed
元の配列 直し方 結果
int[] Arrays.stream(nums).boxed().toList() List<Integer>(不変)
int[] IntStream.of(nums).boxed().collect(Collectors.toCollection(ArrayList::new)) List<Integer>(可変)
Integer[] Arrays.asList(boxed) 要素数3、配列と連動
long[]/double[] Arrays.stream(arr).boxed().toList() 同様

Set.of・Map.of の重複エラーと要素の順序

Set.of と Map.of は、重複した要素・キーを渡すと IllegalArgumentException になります。 また、反復順(for 文で取り出す順)は決まっていません。

// SetMap.java
import java.util.*;

public class SetMap {
    public static void main(String[] args) {
        try { Set.of("a", "b", "a"); } catch (Exception e) { System.out.println("Set.of 重複: " + e); }
        try { Map.of("k1", 1, "k1", 2); } catch (Exception e) { System.out.println("Map.of 重複キー: " + e); }
        try { Set.of("a", null); } catch (Exception e) { System.out.println("Set.of null: " + e); }
        try { Map.of("k", null); } catch (Exception e) { System.out.println("Map.of null値: " + e); }
        System.out.println("Set.copyOf 重複は許容: " + Set.copyOf(List.of("a", "b", "a")).size());
        System.out.println("Map.ofEntries: " + new TreeMap<>(Map.ofEntries(Map.entry("x", 1), Map.entry("y", 2))));
        System.out.println("Set.of 反復順: " + Set.of("apple", "banana", "cherry", "date", "elderberry"));
        System.out.println("Map.of 反復順: " + Map.of("apple", 1, "banana", 2, "cherry", 3, "date", 4));
        System.out.println("順序を保つ: " + new LinkedHashSet<>(List.of("apple", "banana", "cherry", "date", "elderberry")));
        System.out.println("List.of(Set.of) は順序?: " + List.of("c","a","b"));
    }
}

実行結果(OpenJDK 21.0.10、1回目):

Set.of 重複: java.lang.IllegalArgumentException: duplicate element: a
Map.of 重複キー: java.lang.IllegalArgumentException: duplicate key: k1
Set.of null: java.lang.NullPointerException
Map.of null値: java.lang.NullPointerException
Set.copyOf 重複は許容: 2
Map.ofEntries: {x=1, y=2}
Set.of 反復順: [cherry, date, banana, elderberry, apple]
Map.of 反復順: {banana=2, apple=1, cherry=3, date=4}
順序を保つ: [apple, banana, cherry, date, elderberry]
List.of(Set.of) は順序?: [c, a, b]

同じプログラムを続けて3回実行し、反復順の行だけを取り出した結果です。

Set.of 反復順: [apple, elderberry, banana, date, cherry]
Map.of 反復順: {cherry=3, apple=1, banana=2, date=4}
Set.of 反復順: [cherry, apple, elderberry, banana, date]
Map.of 反復順: {banana=2, date=4, cherry=3, apple=1}
Set.of 反復順: [apple, cherry, date, banana, elderberry]
Map.of 反復順: {date=4, banana=2, apple=1, cherry=3}

Set.of/Map.of の反復順は、JVM を起動するたびに変わりました。 Javadoc でも反復順は「未規定で、変わることがある」とされています。順番に依存したテストや表示は、実行ごとに結果が変わる原因になります。

症状 原因 直し方
IllegalArgumentException: duplicate element Set.of に同じ要素 重複がありうるなら Set.copyOf(list) か new HashSet<>(list)
IllegalArgumentException: duplicate key Map.of に同じキー キーを見直す。統合したいなら Collectors.toMap(k, v, (a, b) -> b)
NullPointerException null 要素・null キー・null 値 null を入れない設計にするか HashMap を使う
実行ごとに表示順が変わる 反復順は未規定 挿入順なら LinkedHashSet/LinkedHashMap、並べ替えなら TreeSet/TreeMap
Map.of に11組以上入れたい Map.of は10組まで Map.ofEntries(Map.entry(k, v), ...)

Set.copyOf は重複を許容し、1つにまとめます(実行結果で要素数2)。「リテラルで書くときは重複を早く見つけたい」のが Set.of、「データから作るときは重複をまとめたい」のが Set.copyOf と使い分けます。

Java 21 の SequencedCollection との関係

Java 21 で List に getFirst()/getLast()/reversed()/addFirst() などが加わりました。 不変リストでは、読み取り系は使え、変更系は UnsupportedOperationException になります。Arrays.asList の reversed() は配列とつながったビューです。

// Rev.java
import java.util.*;

public class Rev {
    public static void main(String[] args) {
        List<String> of = List.of("a", "b", "c");
        System.out.println("List.of.reversed() = " + of.reversed() + " getFirst=" + of.getFirst() + " getLast=" + of.getLast());
        try { of.reversed().addFirst("z"); } catch (Exception e) { System.out.println("List.of.reversed().addFirst -> " + e); }
        String[] arr = {"a", "b", "c"};
        List<String> rev = Arrays.asList(arr).reversed();
        rev.set(0, "Z");
        System.out.println("Arrays.asList(arr).reversed().set(0) -> 配列=" + Arrays.toString(arr));
    }
}

実行結果(OpenJDK 21.0.10):

List.of.reversed() = [c, b, a] getFirst=a getLast=c
List.of.reversed().addFirst -> java.lang.UnsupportedOperationException
Arrays.asList(arr).reversed().set(0) -> 配列=[a, b, Z]

逆順ビューの先頭を set すると、元の配列の末尾が Z に変わりました。reversed() もコピーではなくビューです。

Java 25 での違い(公式ドキュメントで確認)

本記事のコードはすべて OpenJDK 21.0.10 で実行しています。Java 25 は手元で実行していません。Java 25 の List の Javadoc を確認したところ、Java 9 以降の追加は of(Java 9)、copyOf(Java 10)、addFirst/addLast/getFirst/getLast/removeFirst/removeLast/reversed(Java 21)で、Java 22〜25 で List に追加されたメソッドはありませんでした。不変リストの説明(null を許可しない、変更操作で UnsupportedOperationException、値ベースである)も同じです。本記事の比較表は Java 25 でもそのまま当てはまります。

なお、不変リストは「値ベース(value-based)」とされており、== での比較や synchronized のロック対象に使わないよう Javadoc に記載されています。本記事の List.copyOf(...) == ... はコピーの有無を観察するためのもので、通常のコードでは equals で比較します。

よくある質問

Q. List.of と Arrays.asList はどちらを使えばよいですか? A. 変更しない固定のリストなら List.of が基本です。null を拒否し、元の配列ともつながらないので意図が明確です。配列とリストを同時に書き換えたい、または null 要素を含む固定長リストが必要なときに Arrays.asList を選びます。

Q. UnsupportedOperationException が出たらどう直しますか? A. 変更したいリストを new ArrayList<>(元のリスト) でコピーしてから add/remove します。スタックトレースに ImmutableCollections が出れば List.of 系、AbstractList が出れば Arrays.asList で作られたリストです。

Q. Collections.unmodifiableList と List.copyOf の違いは? A. unmodifiableList は元リストの読み取り専用ビューで、元を変更すると見える内容も変わります。List.copyOf は独立した不変コピーで、元の変更の影響を受けません。null 要素は前者は許可、後者は NullPointerException です。

Q. Stream.toList() と collect(Collectors.toList()) は同じですか? A. 違います。Stream.toList()(Java 16)は不変リストを返し、null 要素は許可します。Collectors.toList() は Java 21 の実行では変更できる ArrayList が返りましたが、型と可変性は保証されていません。変更するなら Collectors.toCollection(ArrayList::new) を使います。

まとめ

  • List.of は完全な不変リストで、add/remove/set すべて不可、null は作成時も contains(null) も NPE です。
  • Arrays.asList は配列とつながった固定長リストで、set・sort はでき、add/remove は UnsupportedOperationException です。
  • 要素を増減するなら new ArrayList<>(...) でコピーしてから操作します。
  • Collections.unmodifiableList は読み取り専用のビューで、元リストの変更がそのまま見えます。固定したいなら List.copyOf です。
  • Stream.toList() は不変だが null 可、Collectors.toList() は可変性の保証がないので、変更するなら toCollection(ArrayList::new) です。
  • Arrays.asList(int[]) は要素数1の List<int[]> になります。Arrays.stream(arr).boxed().toList() で変換します。
  • Set.of/Map.of は重複で IllegalArgumentException、反復順は実行ごとに変わるので、順序が必要なら LinkedHashSet などを使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java List.ofとArrays.asListの違い|変更可否の比較表」

株式会社RJCでは、Javaのコレクションの正しい使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?