前言
List 與 Set 同屬 Collection 介面的直系子介面,是 Java 集合框架中最常用的兩大支柱。雖然都用於存放物件引用,但其核心語義、底層資料結構與適用場景截然不同。本文從介面契約、實作類別、效能特徵、選型指南四個維度深度對比,助你精準選擇。
一、介面契約核心差異
| 特性 | List | Set |
| 順序性 | 維護插入順序,支援索引存取 | 不保證順序(除 LinkedHashSet/TreeSet) |
| 重複元素 | 允許重複(equals() 為 true) | 嚴格唯一,add() 回傳 false 表示重複 |
| Null 元素 | 允許多個 null | 最多一個 null(HashSet/LinkedHashSet),TreeSet 不允許 |
| 核心契約 | 有序序列 | 數學集合(無序、唯一) |
二、主要實作類別對比
List 實作
| 類別 | 底層結構 | 優勢 | 劣勢 | 適用場景 |
ArrayList | 動態陣列 | 隨機存取 O(1)、記憶體緊湊、遍歷快 | 中間插入/刪除 O(n) 需搬移元素 | 頻繁讀取、較少修改、需索引存取 |
LinkedList | 雙向鏈表 | 頭尾增刪 O(1)、實作 Queue/Deque | 隨機存取 O(n)、記憶體開銷大(節點物件) | 頻繁頭尾操作、佇列/棧、大量中間增刪 |
Vector | 同步動態陣列 | 執行緒安全(舊版 API) | 效能差、已被 Collections.synchronizedList 取代 | 避免使用,歷史遺留專案維護 |
Set 實作
| 類別 | 底層結構 | 優勢 | 劣勢 | 適用場景 |
HashSet | 雜湊表(HashMap) | 增刪查 O(1)、效能最佳 | 無序、需正確實作 hashCode()/equals() | 高頻去重、唯一性檢查、大資料集 |
LinkedHashSet | 雜湊表 + 雙向鏈表 | 維護插入順序、查詢仍 O(1) | 記憶體略大於 HashSet | 需保留插入順序的去重場景 |
TreeSet | 紅黑樹(NavigableMap) | 元素有序(自然序/Comparator)、範圍查詢 | 增刪查 O(log n)、需實作 Comparable/Comparator | 排序去重、區間查詢、最近鄰搜尋 |
EnumSet | 位向量 | 極致效能、型別安全、無垃圾 | 僅適用 enum 類型 | 枚舉值集合操作、狀態機、位遮罩替代 |
三、效能特徵總覽(大 O 複雜度)
| 操作 | ArrayList | LinkedList | HashSet | LinkedHashSet | TreeSet |
| get(index) | O(1) | O(n) | — | — | — |
| add(element) | O(1) 攤銷 | O(1) 尾端 | O(1) | O(1) | O(log n) |
| remove(element) | O(n) | O(n) 查找 | O(1) | O(1) | O(log n) |
| contains(element) | O(n) | O(n) | O(1) | O(1) | O(log n) |
| iterator.remove() | O(n) | O(1) | O(1) | O(1) | O(log n) |
四、選型決策指南
- 需要索引存取、頻繁遍歷、較少中間修改 →
ArrayList
- 頻繁頭尾增刪、實作佇列/棧 →
LinkedList(或 ArrayDeque 更佳)
- 純粹去重、高頻 contains 檢查、不在意順序 →
HashSet
- 去重且需保留插入順序 →
LinkedHashSet
- 去重且需排序/範圍查詢 →
TreeSet
- 元素為枚舉類型 →
EnumSet
- 執行緒安全需求 →
Collections.synchronizedList/Set、CopyOnWriteArrayList、ConcurrentSkipListSet
五、正確覆寫 equals() 與 hashCode() 的鐵律
所有放入 HashSet、LinkedHashSet、或作為 HashMap key 的物件,必須同時覆寫 equals() 與 hashCode(),並遵守契約:
- 若
a.equals(b) 為 true,則 a.hashCode() == b.hashCode() 必為 true。
- 覆寫
equals() 時必須覆寫 hashCode(),反之亦然。
- 建議使用 IDE 自動生成或
Objects.hash()、Objects.equals() 輔助實作。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
六、常見誤區與最佳實務
| 誤區 | 正確做法 |
用 ArrayList.contains() 做大量去重檢查 | 改用 HashSet,O(n) → O(1) |
直接用 new HashSet(list) 去重後順序亂了 | 用 new LinkedHashSet(list) 保留原順序 |
| 自訂類別未覆寫 equals/hashCode 就放入 HashSet | 必須覆寫,否則依預設 identity 比較導致邏輯錯誤 |
| 在 TreeSet 放入未實作 Comparable 且無 Comparator 的物件 | 實作 Comparable 或建構時傳入 Comparator |
| 多執行緒環境直接操作非同步集合 | 用同步包裝或並發集合(java.util.concurrent) |
結語
List 與 Set 代表了資料結構中「序列」與「集合」兩種基本數學抽象。掌握其契約差異、實作特性與效能權衡,是寫出高效、正確 Java 程式的基石。實務中請依業務語義優先選擇介面類型(List vs Set),再根據存取模式選定具體實作,並善用 IDE 重構工具確保 equals/hashCode 正確性。