TreeSet 是 Java 集合框架中唯一同時具備「有序」與「去重」特性的 Set 實現,底層基於紅黑樹(自平衡二叉搜索樹),查詢/插入/刪除均為 O(log n)。本文深度解析其核心原理、自然排序與定製排序兩種模式、自定義對象存儲實戰及常見坑點。
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底層結構 | HashMap (哈希表) | LinkedHashMap (哈希表+雙向鏈表) | TreeMap (紅黑樹) |
| 有序性 | 無序 | 插入序 | 自然序/定製序 (升序) |
| 去重依據 | equals() + hashCode() | equals() + hashCode() | compareTo() / compare() 返回 0 |
| 性能 | O(1) 最快 | O(1) | O(log n) |
| null 元素 | 允許一個 | 允許一個 | 不允許 (拋 NPE) |
元素類實現 Comparable<E> 接口,重寫 compareTo(E o):
JDK 內置類已實現:String、Integer、Double、Date 等均按自然語義排序(字母序、數值序、時間序)。
構造 TreeSet 時傳入 Comparator<E>,適用於:
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
// 方式一:自然排序(Student 實現 Comparable)
TreeSet set1 = new TreeSet<>();
set1.add(new Student("taoshihan1", 30));
set1.add(new Student("taoshihan2", 20));
set1.add(new Student("taoshihan3", 40));
set1.add(new Student("taoshihan4", 30)); // 年齡相同,按姓名排序
System.out.println("=== 自然排序(年齡升序,同齡姓名序)===");
for (Student s : set1) {
System.out.println(s.getName() + " ===" + s.getAge());
}
// 方式二:定製排序(降序,無需修改 Student 類)
TreeSet set2 = new TreeSet<>((s1, s2) -> {
// 年齡降序
int ageDiff = s2.getAge() - s1.getAge();
if (ageDiff != 0) return ageDiff;
// 年齡相同,姓名升序
return s1.getName().compareTo(s2.getName());
});
set2.addAll(set1);
System.out.println("\n=== 定製排序(年齡降序,同齡姓名序)===");
for (Student s : set2) {
System.out.println(s.getName() + " ===" + s.getAge());
}
}
}
// 學生類實現 Comparable 自然排序
class Student implements Comparable {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public int compareTo(Student other) {
// 主鍵:年齡升序
int ageDiff = this.age - other.age;
if (ageDiff != 0) return ageDiff;
// 次鍵:姓名字母序
return this.name.compareTo(other.name);
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
=== 自然排序(年齡升序,同齡姓名序)=== taoshihan2 ===20 taoshihan1 ===30 taoshihan4 ===30 taoshihan3 ===40 === 定製排序(年齡降序,同齡姓名序)=== taoshihan3 ===40 taoshihan1 ===30 taoshihan4 ===30 taoshihan2 ===20
// TreeMap.put() 核心邏輯(TreeSet 內部調用)
public V put(K key, V value) {
// ... 紅黑樹插入邏輯 ...
int cmp = compare(key, existingKey); // 使用 compareTo 或 compare
if (cmp == 0) {
return existingValue; // 視為重複,不插入,返回舊值
}
// ... 插入新節點 ...
}
關鍵結論:TreeSet 判斷重複完全依賴比較器返回 0,不調用 equals()。這意味著:
compareTo 返回 0 但 equals 返回 false → 視為重複,不存入(違反 Set 語義)(a.compareTo(b) == 0) == a.equals(b)(強一致性)| 坑點 | 後果 | 解決方案 |
|---|---|---|
| 存入 null | 拋出 NullPointerException | 不存 null,或用 Collections.emptySet() 替代 |
| compareTo 與 equals 不一致 | Set 行為違反預期,難以調試 | 重寫 compareTo 時同步重寫 equals/hashCode,保持一致 |
| 大量數據插入性能 | O(n log n) 慢於 HashSet | 先用 HashSet 去重,再構建 TreeSet 排序: new TreeSet<>(hashSet) |
| 併發環境使用 | 非線程安全,數據錯亂 | 用 Collections.synchronizedSortedSet() 或 ConcurrentSkipListSet |
| 序列化 | Comparator 若非 static/serializable 導致序列化失敗 | Comparator 實現 Serializable,或用 Lambda 表達式(隱式實現) |
// 線程安全的有序 Set,基於跳表實現,性能優於同步 TreeSet SetconcurrentSet = new ConcurrentSkipListSet<>(); // 支持更高併發吞吐,API 與 TreeSet 基本一致
ConcurrentSkipListSet