the KodeLab

Java の Arrays.asList() と List.of() の違い|可変性・内部実装・使い分け

3,375 文字 9 分で読めます
Java の Arrays.asList() と List.of() の違い|可変性・内部実装・使い分け

Arraysjava.util.Arrays)は Java 1.2 から存在する API です。Listjava.util.List)も同じく Java 1.2 からありますが、その中の List.of() は Java 9 で追加された新しいメソッドです。両者とも 1 行で List を作る便利なショートカットで、呼び出し側のコードからは同じように見えます。しかし返ってくるオブジェクトは別のクラスで、可変性のルールもまったく異なります。本記事では両者の違い、間違えたときに起きること、そしてその違いが生まれる内部実装を解説します。

主な違い

メソッドget(int index)set(int, T obj)add(T obj)remove(int index)
Arrays.asList()
List.of()

Arrays.asList() が返す List の実体は java.util.Arrays$ArrayList です。Arrays 内部に実装された独自の List クラスで、よく使う java.util.ArrayList とは別物です。set() は使えますが、add()remove() は受け付けません。

Arrays.asList() が返したオブジェクトに対して add()remove() を呼ぶと、java.lang.UnsupportedOperationException が投げられます。

Arrays.asList()

まずはシンプルな例を見てみます。

var a = Arrays.asList(0);
System.out.println(a);
// [0]

a.set(0, 1);
System.out.println(a);
// [1]

a.add(2);
// java.lang.UnsupportedOperationException

Arrays.asList() にはもう一つ知っておくべき特徴があります。返ってくる List の中身の配列は、渡した配列そのものだという点です。以下の例のほうが分かりやすいでしょう。

var arr = new Integer[] {5, 5, 5};
var list = Arrays.<Integer>asList(arr);

// arr[1] の中身を確認
System.out.println(arr[1]); // 5

// list 経由で内容を変更
list.set(1, 8);

// もう一度 arr[1] の中身を確認
System.out.println(arr[1]); // 8

これは Arrays.asList() が、渡された配列を ArrayList として薄くラップしているだけだからです。後半の「内部の仕組み」セクションでさらに詳しく説明します。

List.of()

List.of() が返すオブジェクトは java.util.ImmutableCollections 内部のサブクラス由来です。空の配列の場合は java.util.ImmutableCollections$ListN、要素が 1〜2 個の場合は java.util.ImmutableCollections$List12 になります。

これは読み取り専用の List なので、内容を変更するメソッド — set()add()remove() — はすべて java.lang.UnsupportedOperationException を投げます。

var a = List.of(0);
System.out.println(a);
// [0]

a.set(0, 1);
// java.lang.UnsupportedOperationException

自由に変更できる List が欲しい場合は、自分で変換する必要があります。

ミュータブルな List が必要な場合

ArrayList(または LinkedList など、用途に合った List 実装)で包み直します。一見手間に見えますが、こうすることで元のイミュータブル List が絶対に変更されない保証が得られます — それこそが List.of() の存在意義です。

var a = new ArrayList<Integer>(List.of(1, 2));
System.out.println(a);
// [1, 2]

a.add(3); // true ← 成功を返す
System.out.println(a);
// [1, 2, 3]

a.remove(2); // 3 ← 削除された要素を返す
System.out.println(a);
// [1, 2]

a.set(0, 5); // 1 ← 上書きされた旧要素を返す
System.out.println(a);
// [5, 2]

内部の仕組み

ArrayList とは

Java に限らず、コンピュータサイエンスの世界には linked list(連結リスト)array list(配列リスト) という 2 つの基本的なリスト構造があります。

Linked list は、各要素がメモリ上で連続している必要がありません。各要素は自身の値と、次の要素の位置を指すポインタを持ちます。これには 3 つの利点があります。1 つ目は長さが柔軟で、事前に要素数を見積もらなくてもデータがある分だけメモリを使えること。2 つ目はメモリが断片化していても問題ないこと(通常の配列のようにまとまった領域が不要)。3 つ目はリストのどこに要素を挿入/削除するのも簡単で、前の要素のポインタを付け替えるだけで済むこと(C 言語のポインタの概念があると理解しやすい)。デメリットは、要素数が多くなるとランダムアクセスができない点です。つまりインデックスで N 番目の要素に直接アクセスできず、先頭から 1 つずつ辿っていく必要があります。また、各要素にポインタを持たせる分、少しだけメモリを余分に使います。

Array list はメモリ上に連続した領域を必要とし、通常の配列と同じように要素をメモリアドレス順に並べて格納します。利点はランダムアクセスが高速なことで、先頭のアドレスさえ分かれば、単純な足し算と掛け算で N 番目の要素のアドレスが即座に計算できます。デメリットは、内部の配列が満杯になる(デフォルトでは 10 要素)と、より大きな配列(通常は 1.5 倍)を新しく作り、旧配列の内容をコピーして旧配列を捨てる必要がある点です。もう一つのデメリットは、N 番目の位置に要素を挿入/削除するとき、N より後ろの要素をすべて 1 つずつずらす必要があることです。

Linked list の長所は array list の短所を補い、array list の長所は linked list の短所を補う関係になっています。両方を同時に満たすことはできないので、そのコードで何を優先したいか — ランダムアクセスか、途中への挿入/削除か — で使い分ける必要があります。

なぜ Arrays.asList() があるのか

Arrays.asList() の本来の目的は、通常の Java 配列に List としての能力 — iteratorforEach、あるいは List を受け取る API に渡せること — を与えることです。このため、返ってくる ArrayList の内部配列は、渡したその配列そのものになっています。したがって List の長さは変更できませんが、要素の内容は変更でき、その変更はすべて元の配列に反映されます。

なぜ List.of() があるのか

List.of() の主な目的は、変更不可能なデータ構造を作ることです。これはマルチスレッドプログラミングや関数型プログラミングのスタイルで非常に役立ちます。こうしたスタイルでは、共有データは基本的に読み取り専用にし、変更が必要なときは自分でコピーを取って使うのが安全です — Java の ThreadLocal が各スレッドに独立した値を持たせるのと似た考え方です。