[Learn] Java Collections:List、Set、Map 與常用實作
/ 17 min read
Updated:Table of Contents
學會 class 與 OOP 之後,日常 Java 程式碼最常遇到的下一組工具就是 Collections
同樣是保存多筆資料,選擇的 collection 會直接影響資料規則、操作方式與 iteration order
需要保存一串有順序的資料 → List需要從頭尾加入或取出資料 → Deque需要確保資料不重複 → Set需要透過 key 快速找到資料 → Map實際比較 ArrayList、LinkedList、ArrayDeque,以及 Hash、LinkedHash、Tree 三組 Set 與 Map 實作
List 與 Deque 的常用實作
List<E> 是有順序的 collection,可以透過 index 存取元素,也允許重複值
import java.util.ArrayList;import java.util.List;
List<String> names = new ArrayList<>();
names.add("Amy");names.add("Ben");names.add("Amy");
System.out.println(names); // [Amy, Ben, Amy]System.out.println(names.get(0)); // AmySystem.out.println(names.size()); // 3index 從 0 開始
String first = names.get(0);String second = names.get(1);超出有效範圍時會拋出 IndexOutOfBoundsException
names.get(10);List.of(...) 可以快速建立不能新增、刪除或替換元素的 list
List<String> roles = List.of("ADMIN", "EDITOR");
// roles.add("VIEWER"); // UnsupportedOperationException如果需要一份可修改的 copy,可以再交給 ArrayList
List<String> mutableRoles = new ArrayList<>(roles);mutableRoles.add("VIEWER");List.of(...) 不接受 null
ArrayList:一般 List 的預設選擇
日常最常用的實作是 ArrayList
- 依 index 讀取通常很快
- 在尾端新增元素很方便
- 在中間插入或刪除時,後方元素可能需要搬移
常見操作如下
names.add("Cara");names.set(0, "Alice");names.remove("Ben");
boolean containsAmy = names.contains("Amy");int count = names.size();LinkedList 同時實作 List 與 Deque
LinkedList 是 doubly-linked list,每個元素都透過前後連結串在一起
它同時實作 List 與 Deque,所以可以使用 index,也可以操作頭尾兩端
先把它當成一般 List 使用
import java.util.LinkedList;import java.util.List;
List<String> route = new LinkedList<>();
route.add("Taipei");route.add("Taichung");route.add(1, "Taoyuan");
System.out.println(route); // [Taipei, Taoyuan, Taichung]System.out.println(route.get(1)); // Taoyuan
route.remove(1);System.out.println(route); // [Taipei, Taichung]因為左側型別是 List<String>,呼叫端只看得到 List 契約
需要操作頭尾時,可以把 reference type 寫成 Deque<String>
import java.util.Deque;import java.util.LinkedList;
Deque<String> route = new LinkedList<>();
route.addFirst("Taipei");route.addLast("Taichung");route.addLast("Kaohsiung");
System.out.println(route.getFirst()); // TaipeiSystem.out.println(route.getLast()); // Kaohsiung
route.removeFirst();System.out.println(route); // [Taichung, Kaohsiung]LinkedList 沒有像 ArrayList 一樣支援快速 random access
get(index) 需要從較接近的頭或尾沿著 link 尋找,所以大量使用 index 的程式通常更適合 ArrayList
LinkedList 在已經透過 iterator 到達指定位置時,可以直接在附近插入或刪除元素,但日常 List 情境仍然先選 ArrayList 會比較直覺
ArrayDeque 用來實作 queue 與 stack
Deque 是 double-ended queue,可以從頭尾兩端加入、查看或取出元素
ArrayDeque 使用可擴充 array 實作 Deque,不提供 index 存取,也不接受 null
當成 FIFO queue
FIFO 是 First In, First Out,先加入的元素會先被取出
import java.util.ArrayDeque;import java.util.Deque;
Deque<String> waiting = new ArrayDeque<>();
waiting.offerLast("Amy");waiting.offerLast("Ben");waiting.offerLast("Cara");
String next = waiting.pollFirst();String following = waiting.peekFirst();
System.out.println(next); // AmySystem.out.println(following); // BenSystem.out.println(waiting); // [Ben, Cara]offerLast從尾端加入pollFirst從頭端取出並移除peekFirst查看頭端但不移除
可以把它想成排隊
取出方向 ← [Amy, Ben, Cara] ← 加入方向Amy 最早加入,所以第一次 pollFirst() 會得到 Amy
當成 LIFO stack
LIFO 是 Last In, First Out,最後放入的元素會先被取出
import java.util.ArrayDeque;import java.util.Deque;
Deque<String> pages = new ArrayDeque<>();
pages.push("首頁");pages.push("商品頁");pages.push("結帳頁");
String current = pages.pop();String previous = pages.peek();
System.out.println(current); // 結帳頁System.out.println(previous); // 商品頁System.out.println(pages); // [商品頁, 首頁]push把元素放到 stack 頂端pop取出並移除頂端元素peek查看頂端但不移除
可以把它想成疊盤子
頂端 → [結帳頁, 商品頁, 首頁]最後放入的結帳頁位於頂端,所以第一次 pop() 會先得到結帳頁
Java 雖然有舊的 Stack class,現代程式通常使用 Deque 與 ArrayDeque
空 Deque 的兩組 API
Deque 提供兩種風格的操作
| 操作 | 失敗時拋出 exception | 失敗時回傳特殊值 |
|---|---|---|
| 加入頭端 | addFirst | offerFirst |
| 加入尾端 | addLast | offerLast |
| 取出頭端 | removeFirst | pollFirst |
| 取出尾端 | removeLast | pollLast |
| 查看頭端 | getFirst | peekFirst |
| 查看尾端 | getLast | peekLast |
在空 Deque 呼叫 removeFirst、getFirst 或 pop 會拋出 NoSuchElementException
pollFirst、peekFirst 或 poll 則會回傳 null
因為 ArrayDeque 不接受 null element,所以 null 可以清楚代表目前沒有元素
ArrayList、LinkedList 與 ArrayDeque 如何選
| 實作 | 實作的主要 interface | 適合情境 | 注意事項 |
|---|---|---|---|
ArrayList | List | index 存取、依序走訪、尾端新增 | 中間插入刪除可能搬移元素 |
LinkedList | List、Deque | 確實需要 linked list,或透過 iterator 頻繁修改目前位置 | index 存取需要 traversal,每個元素需要 node |
ArrayDeque | Deque | FIFO queue、LIFO stack、操作頭尾兩端 | 沒有 index、不接受 null、不是 thread-safe |
大多數一般 List 用途先選 ArrayList
需要 queue、stack 或雙端操作時先選 ArrayDeque
只有在 LinkedList 的特性真的符合操作模式時才選 LinkedList,不要只因為名稱有 list 就預設使用它
Set 保存不重複的資料
Set<E> 不允許重複元素,適合保存 tag、權限、已處理 ID 等資料
import java.util.HashSet;import java.util.Set;
Set<String> tags = new HashSet<>();
boolean firstAdd = tags.add("java");boolean secondAdd = tags.add("java");
System.out.println(firstAdd); // trueSystem.out.println(secondAdd); // falseSystem.out.println(tags.size()); // 1add 回傳 false 代表相同元素已經存在
常見實作的差異如下
| 實作 | iteration order | 一般 add/contains | null | 常見用途 |
|---|---|---|---|---|
HashSet | 不保證 | 平均 O(1) | 接受一個 null element | 一般去重與 membership check |
LinkedHashSet | insertion order | 平均 O(1) | 接受一個 null element | 去重後仍要維持加入順序 |
TreeSet | sorted order | O(log n) | natural order 不接受 null | 需要排序與範圍查詢 |
Set.of(...) 可以快速建立不能修改的 set
Set<String> permissions = Set.of("READ", "WRITE");Set.of(...) 不接受 null,建立時傳入重複元素也會拋出 IllegalArgumentException
HashSet:只在意唯一性
HashSet 適合只在意元素是否存在,不在意 iteration order 的情境
import java.util.HashSet;import java.util.Set;
Set<String> tags = new HashSet<>();
tags.add("spring");tags.add("java");tags.add("backend");tags.add("java");
System.out.println(tags.contains("java")); // trueSystem.out.println(tags.size()); // 3System.out.println(tags); // iteration order 不保證最後一次加入 java 不會產生第二個元素
不要根據某一次執行看到的輸出順序撰寫程式,換資料、換 JDK 或重新配置內部 table 後,iteration order 都可能改變
HashSet 判斷自訂物件是否相同時,會使用 equals 與 hashCode
如果兩個物件在業務上應該視為相同,卻沒有正確實作這兩個 method,HashSet 可能無法如預期去重
LinkedHashSet:去重並保留加入順序
LinkedHashSet 在 hash table 之外維護元素的前後關係,因此 iteration 時會按照 insertion order
import java.util.LinkedHashSet;import java.util.List;import java.util.Set;
List<String> input = List.of( "banana", "apple", "orange", "apple");
Set<String> unique = new LinkedHashSet<>(input);
System.out.println(unique); // [banana, apple, orange]第二個 apple 被移除,第一次出現的位置則保留下來
這種特性很適合處理「依原始順序去重」
List<String> uniqueList = List.copyOf(new LinkedHashSet<>(input));如果只用 HashSet 去重再轉回 List,就不能依賴結果仍然維持原始順序
TreeSet:唯一值加上排序
TreeSet 會按照 natural order 或傳入的 Comparator 排序
它也實作 NavigableSet,可以查找最接近指定條件的元素
import java.util.List;import java.util.NavigableSet;import java.util.TreeSet;
NavigableSet<Integer> scores = new TreeSet<>( List.of(90, 60, 75, 60));
System.out.println(scores); // [60, 75, 90]System.out.println(scores.ceiling(70)); // 75System.out.println(scores.higher(75)); // 90ceiling(70)找大於或等於70的最小元素higher(75)找嚴格大於75的最小元素- 重複的
60只會保留一份
String 的 natural order 是字典順序,也可以自己指定排序規則
import java.util.Comparator;import java.util.Set;import java.util.TreeSet;
Set<String> namesByLength = new TreeSet<>( Comparator.comparingInt((String name) -> name.length()) .thenComparing(Comparator.naturalOrder()));
namesByLength.add("Cara");namesByLength.add("Amy");namesByLength.add("Alice");namesByLength.add("Ben");
System.out.println(namesByLength); // [Amy, Ben, Cara, Alice]先比較字串長度,相同長度時再依 natural order 排序
對 TreeSet 來說,Comparator.compare(a, b) == 0 就代表兩個元素相同
如果只比較字串長度,Amy 與 Ben 都是三個字元,其中一個會被視為重複值
因此需要同時保留相同長度的不同名稱時,要像範例一樣加入 thenComparing
Map 使用 key 找到 value
Map<K, V> 保存 key 與 value 的對應關係
import java.util.HashMap;import java.util.Map;
Map<String, String> namesById = new HashMap<>();
namesById.put("S001", "Amy");namesById.put("S002", "Ben");
String name = namesById.get("S001");System.out.println(name); // AmyMap 的 key 不能重複,再次放入相同 key 會取代舊 value
namesById.put("S001", "Alice");
System.out.println(namesById.get("S001")); // Alice常見操作如下
boolean hasStudent = namesById.containsKey("S001");String fallback = namesById.getOrDefault("S999", "查無學生");namesById.remove("S002");Map.get(key) 找不到時會回傳 null
如果 map 本身允許 null value,只看 get 無法分辨是沒有這個 key,還是 key 對應到 null
需要區分時可以使用 containsKey
需要依 key 建立並累積 list 時,computeIfAbsent 很方便
Map<String, List<String>> studentsByClass = new HashMap<>();
studentsByClass .computeIfAbsent("A", key -> new ArrayList<>()) .add("Amy");第一次遇到 A 時會建立新的 ArrayList,之後再遇到 A 就重用既有 list
Map.of(...) 可以建立不能修改的 map,同樣不接受 null key 或 null value
常見實作如下
| 實作 | key iteration order | 一般 get/put | null | 常見用途 |
|---|---|---|---|---|
HashMap | 不保證 | 平均 O(1) | 接受一個 null key 與 null value | 一般 key-value 查找 |
LinkedHashMap | insertion 或 access order | 平均 O(1) | 接受一個 null key 與 null value | 需要穩定順序或簡單 cache |
TreeMap | key 的 sorted order | O(log n) | natural order 不接受 null key,接受 null value | 排序與範圍查詢 |
HashMap:一般 key-value 查找
HashMap 是最常見的 Map 實作,適合不需要 iteration order 的一般查找
import java.util.HashMap;import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
scores.put("S003", 90);scores.put("S001", 70);scores.put("S002", 80);
System.out.println(scores.get("S002")); // 80System.out.println(scores.containsKey("S004")); // false
for (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue());}entrySet() 的 iteration order 不保證,不要假設輸出一定是 S001、S002、S003
再次放入相同 key 會更新 value,不會增加新的 entry
Integer previous = scores.put("S002", 85);
System.out.println(previous); // 80System.out.println(scores.get("S002")); // 85System.out.println(scores.size()); // 3HashMap 判斷 key 是否相同也依賴 equals 與 hashCode
如果使用自訂物件當 key,參與 equality 的內容放入 map 後不應再改變
LinkedHashMap:保留 insertion order
LinkedHashMap 預設按照 key 第一次插入的順序 iteration
import java.util.LinkedHashMap;import java.util.Map;
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("S003", 90);scores.put("S001", 70);scores.put("S002", 80);
System.out.println(scores.keySet()); // [S003, S001, S002]
scores.put("S001", 95);
System.out.println(scores.keySet()); // [S003, S001, S002]System.out.println(scores); // {S003=90, S001=95, S002=80}更新既有 key 的 value 不會改變它第一次插入的位置
這很適合需要穩定輸出順序的情境,例如依讀取順序產生報表
LinkedHashMap 也能選擇 access-order mode
import java.util.LinkedHashMap;
LinkedHashMap<String, String> cache = new LinkedHashMap<>( 16, 0.75f, true);
cache.put("A", "Apple");cache.put("B", "Banana");cache.put("C", "Cherry");
cache.get("A");
System.out.println(cache.keySet()); // [B, C, A]第三個 constructor argument 設為 true 後,最近被存取的 A 會移到 iteration order 最後方
這個模式可以作為 LRU cache 的基礎,但 LinkedHashMap 本身不是 thread-safe,完整 cache 還需要處理容量、concurrency 與資料載入策略
TreeMap:按照 key 排序
TreeMap 會按照 key 的 natural order 或指定的 Comparator 排序
它也實作 NavigableMap,可以依 key 做範圍與相鄰查詢
import java.util.NavigableMap;import java.util.TreeMap;
NavigableMap<String, Integer> scores = new TreeMap<>();
scores.put("S003", 90);scores.put("S001", 70);scores.put("S002", 80);
System.out.println(scores); // {S001=70, S002=80, S003=90}System.out.println(scores.firstEntry()); // S001=70System.out.println(scores.higherEntry("S002")); // S003=90需要反向排序時,可以傳入 Comparator
import java.util.Comparator;import java.util.Map;import java.util.TreeMap;
Map<String, Integer> descending = new TreeMap<>( Comparator.reverseOrder());
descending.put("S001", 70);descending.put("S003", 90);descending.put("S002", 80);
System.out.println(descending.keySet()); // [S003, S002, S001]TreeMap 使用比較結果判斷 key 是否相同
Comparator.compare(a, b) == 0 時,後放入的 value 會取代同一個排序位置的舊 value
自訂 Comparator 時,最好讓排序結果與 key 的 equals 一致,避免 Map 行為難以理解
List、Set 與 Map 如何選
可以先問資料最重要的存取方式是什麼
| 問題 | 選擇 |
|---|---|
| 需要保留加入順序或透過 index 存取 | List |
| 只在意元素是否存在,而且不能重複 | Set |
| 需要用唯一 key 找到 value | Map |
| 同時需要順序與快速查找 | 視需求組合 List 與 Map |
不要只因為 List 最熟悉,就把所有資料都放進 List 再逐筆搜尋
選對 collection 能直接表達資料規則
Set 表達唯一性,Map 表達 key 與 value 的對應,通常也比額外的 if 更清楚
常見錯誤
把 List.of 當成可修改的 ArrayList
List<String> names = List.of("Amy", "Ben");names.add("Cara"); // UnsupportedOperationException需要修改時建立 mutable copy
List<String> names = new ArrayList<>(List.of("Amy", "Ben"));依賴 HashSet 或 HashMap 的 iteration order
HashSet 與 HashMap 不保證 iteration order
順序是需求的一部分時,明確使用 LinkedHashSet、LinkedHashMap、TreeSet 或 TreeMap
使用 mutable object 當作 HashMap key
如果 key 參與 equals 或 hashCode 的內容在放入 map 後改變,之後可能再也找不到原本的 entry
Map key 適合使用 immutable 且 equality 穩定的型別
只因為中間插入就選 LinkedList
LinkedList 的 add(index, value) 仍然要先 traversal 到指定 index
只有透過 iterator 已經位於修改位置時,linked structure 才能避免搬移後方元素
一般 List 使用情境先選 ArrayList,queue 或 stack 則先選 ArrayDeque
結論
- ArrayList 適合大多數一般 List 使用情境
- ArrayDeque 適合 FIFO queue、LIFO stack 與雙端操作
- LinkedList 同時實作 List 與 Deque,但要確認 linked structure 真的符合操作模式
- HashSet 與 HashMap 適合不需要 iteration order 的一般查找
- LinkedHashSet 與 LinkedHashMap 適合需要 insertion order 的資料
- TreeSet 與 TreeMap 適合需要排序、範圍或相鄰查詢的資料
- 選擇 collection 時先表達資料規則,再考慮實作細節與效能
參考資料
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/List.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ArrayList.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedList.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Deque.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ArrayDeque.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Set.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/HashSet.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedHashSet.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/TreeSet.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Map.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/HashMap.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedHashMap.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/TreeMap.html