| 特性 | HashSet | LinkedHashSet | TreeSet |
| 底層結構 | HashMap (數組+鏈表/紅黑樹) | HashMap + 雙向鏈表 | TreeMap (紅黑樹) |
| 順序 | 無序 (隨機) | 插入順序 / 訪問順序 | 自然順序 / 比較器順序 |
| 增刪查複雜度 | O(1) 平均 | O(1) 平均 | O(log n) |
| null 支援 | ✅ 1 個 | ✅ 1 個 | ❌ (拋 NPE) |
| 線程安全 | ❌ | ❌ | ❌ |
| 序列化支援 | ✅ | ✅ | ✅ |
| 記憶體開銷 | 中等 | 較高 (雙向鏈表節點) | 高 (樹節點) |
HashSet / LinkedHashSet 去重原理
// HashMap.putVal 邏輯簡化
public boolean add(E e) {
return map.put(e, PRESENT) == null; // PRESENT = new Object()
}
// 判斷相等兩步驟:
// 1. hashCode() 相同 → 同一桶位
// 2. equals() 返回 true → 認定重複
關鍵規約 (Effective Java Item 11)
- 覆寫 equals 必須覆寫 hashCode
- equals 相等 → hashCode 必相等
- hashCode 相等 ≠ equals 相等 (哈希碰撞)
- 集合中元素不可變性**:作為 Set 元素/Map Key 的對象,**一旦放入不應修改參與 equals/hashCode 的字段**,否則導致「找不到元素」/內存洩漏
不可變性違反示例
class Person { String name; int age; /* equals/hashCode 基於 name+age */ }
Set set = new HashSet<>();
Person p = new Person("Alice", 20);
set.add(p);
p.setAge(21); // 修改參與 hashCode 的字段!
set.contains(p); // false - 元素「丟失」
set.remove(p); // false - 無法刪除 → 內存洩漏
TreeSet 排序機制詳解
方式一:自然順序 (元素實現 Comparable)
class Student implements Comparable {
String name; int score;
public int compareTo(Student o) {
// 按分數降序,分數相同按姓名升序
int cmp = Integer.compare(o.score, this.score);
return cmp != 0 ? cmp : this.name.compareTo(o.name);
}
}
Set set = new TreeSet<>(); // 自動按 compareTo 排序
方式二:比較器順序 (構造器注入 Comparator)
// 靜態比較器 (推薦)
static final Comparator BY_NAME_THEN_SCORE = Comparator
.comparing(Student::getName)
.thenComparingInt(Student::getScore);
Set set = new TreeSet<>(BY_NAME_THEN_SCORE);
// 或 Lambda 內聯
Set set2 = new TreeSet<>((s1, s2) -> s1.getName().compareTo(s2.getName()));
比較器一致性要求
compare(x,y) == -compare(y,x) (反對稱)
compare(x,y)>0 && compare(y,z)>0 → compare(x,z)>0 (傳遞性)
compare(x,y)==0 → (compare(x,z)==compare(y,z)) (與 equals 一致性,**強烈建議**)
- 違反將導致
TreeSet 行為未定義 (拋異常、死循環、數據錯誤)
併發安全 Set 選型
| 方案 | 適用場景 | 特點 |
Collections.synchronizedSet(new HashSet<>()) | 低併發、整體鎖 | 全鎖、擴展性差 |
ConcurrentHashMap.newKeySet() (JDK 8+) | 高併發、只需 Key 集合 | 分段鎖、弱一致性迭代器 |
ConcurrentHashMap.newKeySet(defaultValue) | 需 Map 值語義 | 值固定為 defaultValue |
| CopyOnWriteArraySet | 讀多寫極少、遍歷無鎖 | 寫入複製數組、內存佔用大 |
效能測試基準 (參考)
// JMH 基準測試模板
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class SetBenchmark {
@Param({"1000", "10000", "100000"}) int size;
Set hashSet, linkedSet, treeSet;
@Setup public void setup() {
hashSet = new HashSet<>(size);
linkedSet = new LinkedHashSet<>(size);
treeSet = new TreeSet<>();
for (int i = 0; i < size; i++) {
int v = ThreadLocalRandom.current().nextInt();
hashSet.add(v); linkedSet.add(v); treeSet.add(v);
}
}
@Benchmark public void contains_HashSet(Blackhole bh) { bh.consume(hashSet.contains(500)); }
@Benchmark public void contains_TreeSet(Blackhole bh) { bh.consume(treeSet.contains(500)); }
@Benchmark public void iterate_HashSet(Blackhole bh) { for (int v : hashSet) bh.consume(v); }
@Benchmark public void iterate_LinkedSet(Blackhole bh) { for (int v : linkedSet) bh.consume(v); }
}
選型決策樹
- **需要排序/範圍查詢** (first/last/headSet/tailSet/subSet) → TreeSet
- **需要保持插入順序迭代** (LRU 緩存、日誌去重) → LinkedHashSet
- **純去重、高頻增刪查、無序需求** → HashSet (默認首選)
- **高併發** → ConcurrentHashMap.newKeySet()
- **元素不可比較、需自定義排序** → TreeSet + Comparator
- **元素可能為 null** → 避開 TreeSet
總結
Set 三巨頭各有千秋:**HashSet 通用王者、LinkedHashSet 有序去重、TreeSet 範圍查詢專家**。核心避坑點:元素不可變性、equals/hashCode 契約、TreeSet 比較器一致性、併發場景選型。掌握底層 HashMap/TreeMap 結構,能在面對海量去重、排序、併發場景時做出正確架構決策。
- 上一篇文章: Java 資源文件路徑載入完整指南:ClassLoader 與 Class.getResource() 深度解析
- 下一篇文章: Java ZIP 檔案自動解壓縮工具類實作(支援目錄結構保留)
Copyright ©
程式碼之家 All Rights Reserved