Java 集合框架核心對比:List 與 Set 行為差異
List 與 Set 是 Java 集合框架中最基礎的兩大介面,其核心差異在於 有序性 與 唯一性。本文通過程式碼演示與原理分析,徹底釐清兩者行為邊界。
一、核心特性對比表
| 特性 | List (ArrayList/LinkedList) | Set (HashSet/LinkedHashSet/TreeSet) |
| 元素順序 | 維護插入順序(有序) | HashSet 無序,LinkedHashSet 維護插入順序,TreeSet 自然排序 |
| 重複元素 | 允許重複 | 不允許重複(唯一性) |
| 索引存取 | 支援 get(index) O(1)/O(n) | 不支援索引存取 |
| Null 元素 | 允許多個 null | 最多允許一個 null (HashSet/LinkedHashSet),TreeSet 不允許 |
| 底層結構 | 動態陣列/雙向鏈表 | HashMap/TreeMap |
| 典型實作 | ArrayList, LinkedList, Vector | HashSet, LinkedHashSet, TreeSet |
二、程式碼演示:重複元素添加行為
import java.util.*;
public class ListVsSetDemo {
public static void main(String[] args) {
// 測試資料:包含重複的 "a"
String[] items = {"A", "a", "c", "C", "a"};
System.out.println("=== List (ArrayList) 行為 ===");
List list = new ArrayList<>();
for (String item : items) {
boolean added = list.add(item);
System.out.printf("添加 '%s':%s,當前列表:%s%n",
item, added ? "成功" : "失敗", list);
}
System.out.println("最終 List 大小:" + list.size()); // 5
System.out.println("\n=== Set (HashSet) 行為 ===");
Set set = new HashSet<>();
for (String item : items) {
boolean added = set.add(item);
System.out.printf("添加 '%s':%s,當前集合:%s%n",
item, added ? "成功" : "失敗", set);
}
System.out.println("最終 Set 大小:" + set.size()); // 4
System.out.println("\n=== LinkedHashSet 保持插入順序 ===");
Set linkedSet = new LinkedHashSet<>();
linkedSet.addAll(Arrays.asList(items));
System.out.println("元素順序:" + linkedSet); // [A, a, c, C]
System.out.println("\n=== TreeSet 自然排序 ===");
Set treeSet = new TreeSet<>();
treeSet.addAll(Arrays.asList(items));
System.out.println("排序順序:" + treeSet); // [A, C, a, c] (ASCII 碼順序)
}
}
三、輸出結果分析
=== List (ArrayList) 行為 ===
添加 'A':成功,當前列表:[A]
添加 'a':成功,當前列表:[A, a]
添加 'c':成功,當前列表:[A, a, c]
添加 'C':成功,當前列表:[A, a, c, C]
添加 'a':成功,當前列表:[A, a, c, C, a]
最終 List 大小:5
=== Set (HashSet) 行為 ===
添加 'A':成功,當前集合:[A]
添加 'a':成功,當前集合:[A, a]
添加 'c':成功,當前集合:[A, a, c]
添加 'C':成功,當前集合:[A, a, c, C]
添加 'a':失敗,當前集合:[A, a, c, C]
最終 Set 大小:4
=== LinkedHashSet 保持插入順序 ===
元素順序:[A, a, c, C]
=== TreeSet 自然排序 ===
排序順序:[A, C, a, c]
四、核心機制深度解析
1. List 允許重複的原因
- 底層陣列/鏈表直接儲存引用,不做唯一性檢查
add(E e) 永遠回傳 true,直接在末尾/指定位置插入
- 時間複雜度:ArrayList 尾部 O(1) 攤還,指定位置 O(n)
2. Set 唯一性保證機制
// HashSet.add() 核心邏輯 (JDK 1.8)
public boolean add(E e) {
return map.put(e, PRESENT) == null; // 底層用 HashMap 實現
}
// HashMap.put() 判斷鍵值相等:
// 1. hashCode() 相同
// 2. equals() 回傳 true
// → 視為重複鍵,覆蓋舊值,回傳舊值 → add() 回傳 false
3. 正確覆寫 equals() 與 hashCode() 的關鍵性
public class User {
private Long id;
private String name;
// 必須同時覆寫!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// 錯誤示範:僅覆寫 equals() 不覆寫 hashCode()
// → 同樣邏輯相等的物件,hashCode 不同 → HashSet 視為不同元素 → 違反 Set 契約
五、常見實作類別選型指南
List 選型
| 實作 | 適用場景 | 避免場景 |
| ArrayList | 隨機存取頻繁、尾部新增、記憶體敏感 | 頻繁頭部/中間插刪 |
| LinkedList | 頻繁頭部/中間插刪、佇列/棧操作 | 隨機存取、記憶體敏感 |
| Vector | 舊版 API 相容、執行緒安全需求 | 新專案(用 Collections.synchronizedList 或 CopyOnWriteArrayList) |
Set 選型
| 實作 | 適用場景 | 避免場景 |
| HashSet | 高效能去重、順序無關 | 需保持插入順序、需排序 |
| LinkedHashSet | 去重 + 保持插入順序(LRU 快取) | 需自然排序 |
| TreeSet | 去重 + 自然排序/自定義 Comparator | 高頻寫入(O(log n))、無序場景 |
| EnumSet | Enum 類型專用、位元級效能 | 非 Enum 類型 |
| CopyOnWriteArraySet | 讀多寫少、執行緒安全遍歷 | 高頻寫入、大資料量 |
六、效能複雜度速查
| 操作 | ArrayList | LinkedList | HashSet | TreeSet |
| add(尾部) | O(1)* | O(1) | O(1)* | O(log n) |
| add(指定位置) | O(n) | O(n) | N/A | N/A |
| get(index) | O(1) | O(n) | N/A | N/A |
| contains | O(n) | O(n) | O(1)* | O(log n) |
| remove(Object) | O(n) | O(n) | O(1)* | O(log n) |
| 遍歷 | O(n) | O(n) | O(n) | O(n) |
* 為攤還/平均複雜度,HashSet 最壞 O(n)(哈希衝突)
七、最佳實踐建議
- 需求:去重 →
new HashSet<>(list) 或 list.stream().distinct().collect(toList())
- 需求:去重 + 保持順序 →
new LinkedHashSet<>(list)
- 需求:排序 + 去重 →
new TreeSet<>(list) 或 list.stream().distinct().sorted().collect(toList())
- 需求:頻繁 contains 檢查 → 轉為
HashSet 後查詢 O(1)
- 自訂物件進 Set → 必須 正確覆寫
equals() 與 hashCode()
- 執行緒安全 →
Collections.synchronizedSet(new HashSet<>()) 或 CopyOnWriteArraySet
List 與 Set 的選擇本質上是業務語義的映射:需要順序與重複用 List,需要唯一性用 Set。理解底層 HashMap/Array 實現機制,能避免常見的 equals/hashCode 缺陷導致的業務 Bug。