Arrays全名是java.util.Arrays,是自Java 1.2就有的API。而List(java.util.List)雖然也是Java 1.2就開始有的API,其中的List.of()卻是Java 9才新增的方法。Arrays.asList()與List.of()兩種都是快速建立List物件的方法,但使用上有不太一樣的地方。本文講解兩種List物件的差異,以及為什麼會有這兩種方式產生串列。

差異解說

先上一張表快速看出兩者的差別。

Methodget(int index)set(int, T obj)add(T obj)remove(int index)
Arrays.asList()OOXX
List.of()OXXX

Arrays.asList()得到的物件是來自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()有一個特色,就是透過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,其他陣列是java.util.ImmutableCollections$List12。

這是一個唯讀的List,因此任何會修改到內容的方法都不能使用,會拋出java.lang.UnsupportedOperationException

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

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

如果想要可以自由修改的陣列,就必須自己轉換一下。

如果想要一個可操作的List

我們必須使用原本的List來產生一個新的ArrayList(或是LinkedList之類的)才能修改內容。這樣看起來有點麻煩,這是為了保證經過複雜的運算後不會遇到一些怪事。

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(陣列串列)兩種概念。

Linked List 在記憶體中每個元素可以是分散不連續的,因此每個元素除了紀錄自己是什麼內容,還會紀錄自己的下一個元素在什麼位置。主要好處有三個,一是Linked List的長度很彈性,有多少資料就耗費多少記憶體,不用預估會有多少元素;二是Linked List的使用的記憶體可以很零碎,不像一般的陣列一樣需要一整塊完整的記憶體空間;三是要插入或刪除List中任何一個元素都很簡單,讓前一個元素的指標換一下位置就好(這需要一點C語言指標的概念會比較好理解)。壞處是元素很多的時候沒辦法隨機存取,也就是我們不能透過索引讀取第N個元素,因為每個元素只知道自己的下一個元素是誰,必須一個個數過去;另外就是需要多一點點記憶體空間來紀錄每個元素的下一個位置。

Array List 需要記憶體中連續的記憶體空間,儲存的方式就像一般的陣列一樣照著記憶體位置一一排列。好處是隨機讀取很快速,我們只需要知道第一個元素在什麼位置,就可以透過簡單的加法與乘法立刻知道第N個元素的位置。壞處是Array List裝滿的時候(通常預設是10個)就要在內部重新產生一個更大的Array(通常是1.5倍),然後將舊陣列複製到新陣列再丟棄舊陣列;另一個壞處是插入、刪除其中第N個元素的時候,需要把第N個元素之後的每個元素都一動一次。

可以說Linked List的優點就是補足Array List的缺點,Array List的優點就是補足Linked List的缺點。有一好沒兩好,需要看當下比較注重什麼需求來決定要使用哪一個List。

為什麼會有Arrays.asList?

使用Arrays.asList()主要的目的其實是為了讓一般的Java Array擁有List的能力,像是iteratorforEach等,或是傳送給其他接受List的程式碼。因此Arrays.asList()返回的ArrayList內部使用的陣列就是我們傳進去的那個陣列,因此無法變更List長度,卻可以修改元素內容,而且任何修改都會反映到原本傳進去的陣列上。

為什麼會有List.of?

使用List.of()主要目的是建立不可變更的資料結構,這在Multi-Thread(多執行緒)與Functional(函數導向)的程式設計模式中非常好用。在這兩種設計方式中,大部分的時候對共享的資料最好的運用方式就是只能唯讀,需要更改資料就自己複製一份備份出來使用,像是Java的ThreadLocal那樣。