概述
TreeSet 是 Java 集合框架中 Set 接口下唯一一个保证元素处于排序状态的实现类。它以委托机制将全部存储逻辑交给内部的 TreeMap,利用红黑树的自平衡特性维护键的有序性,并将元素作为键、共享一个哑元值,从而实现了有序、去重的集合。本文将从源码视角(基于 JDK 8)系统揭示 TreeSet 的底层存储结构、排序去重原理、自平衡过程、导航方法与范围视图的实现细节,并经由与 HashSet、LinkedHashSet、ConcurrentSkipListSet 的全维对比,帮助你构建从理论到实践的全链路认知。
- 基于 TreeMap 的红黑树实现:TreeSet 内部组合
NavigableMap(实际为TreeMap),元素作为 Key,共享哑元对象PRESENT,利用红黑树的自平衡特性维护有序性。 - 两种排序模式:自然排序(元素实现
Comparable)与比较器排序(构造时传入Comparator),Comparator优先级高于Comparable。 - 去重依赖比较而非哈希:TreeSet 通过
compareTo或compare返回 0 判定元素相等,而非equals/hashCode,需保持比较器与equals的一致性。 - 有序导航与范围视图:提供
lower/floor/ceiling/higher等导航方法,以及subSet/headSet/tailSet等范围视图,支持高效的范围查找。 - 较低的基本操作性能:增删查时间复杂度 O(log n),低于 HashSet 的 O(1),但支持有序遍历和范围操作,适用于需要排序的场景。
下面是本文的全文组织架构图,它展示了从基础认知到面试专题的六大篇章递进关系:
flowchart TB
subgraph Part1["Part 1: 基础认知篇"]
A1["模块1: 定义、特性与适用场景"]
A2["模块2: 接口与继承体系"]
end
subgraph Part2["Part 2: 存储与构造篇"]
B1["模块3: 存储结构与底层依赖"]
B2["模块4: 构造方法"]
end
subgraph Part3["Part 3: 核心原理篇"]
C1["模块5: add操作"]
C2["模块6: remove与contains"]
C3["模块7: 导航方法与范围视图"]
end
subgraph Part4["Part 4: 迭代与序列化篇"]
D1["模块8: 迭代器与有序遍历"]
D2["模块9: 序列化与克隆"]
end
subgraph Part5["Part 5: 对比与陷阱篇"]
E1["模块10: vs HashSet/LinkedHashSet"]
E2["模块11: vs ConcurrentSkipListSet"]
E3["模块12: 常见陷阱与最佳实践"]
end
subgraph Part6["Part 6: 总结与面试篇"]
F1["模块13: 性能总结"]
F2["模块14: 面试高频专题"]
end
Part1 --> Part2 --> Part3 --> Part4 --> Part5 --> Part6
架构图说明:
- 第一层(Part 1~Part 6) 呈现了从“是什么”到“怎么用”再到“为什么”和“如何选”的认知闭环。读者将沿“基础认知→存储构造→核心原理→迭代与序列化→对比与陷阱→总结与面试”的顺序逐步深入。
- 第二层(模块分布) 中,Part 1 建立概念骨架,明确 TreeSet 在集合框架中的定位;Part 2 深入到类字段与构造器,揭开委托机制的实现细节;Part 3 是全文核心,通过源码级分析
add、remove、导航方法,展现红黑树自平衡与有序操作的运作机理;Part 4 补充迭代器、序列化等辅助机制;Part 5 通过横向对比与陷阱剖析帮助读者进行工程选型与避坑;Part 6 用性能总结和独立面试专题将知识转化为实战竞争力。 - 数据结构映射 上,
subgraph代表篇章,内部节点代表具体模块,流动方向展示了由表及里、由实现到应用的递进关系,与《Set 系列排序集合核心篇章》的定位高度契合。
Part 1:基础认知篇
模块 1:定义、核心特性与适用场景
定义
TreeSet 是 基于红黑树(TreeMap)实现的有序、不包含重复元素的集合。它实现了 NavigableSet 接口,支持以自然顺序或自定义 Comparator 对元素进行排序。
核心特性
- 有序性:元素按升序或指定比较器排序,迭代顺序可预测。
- 基于比较去重:通过
compareTo或compare返回值是否为 0 来判断重复,而非equals/hashCode。 - O(log n) 时间性能:增删查均在对数时间内完成,但常数因子高于
HashSet。 - 导航与范围视图:提供
lower、floor、ceiling、higher等导航方法,以及subSet/headSet/tailSet等视图。 - 非线程安全:多线程并发修改可能抛出
ConcurrentModificationException。 - Null 元素容忍度:取决于比较器。若使用自然排序,
null无法与任何对象比较,会抛NullPointerException;若比较器支持null,则允许插入null(但TreeMap的默认比较器依旧不允许,通常不建议)。 - 依赖 TreeMap:内部所有操作均委托给
NavigableMap<E,Object>实现,实质为TreeMap。
适用场景决策树
flowchart TD
Start["需要一个Set实现"] --> Q1{"是否需要排序?"}
Q1 -- 否 --> Q2{"是否需要保持插入顺序?"}
Q2 -- 否 --> HashSet["HashSet<br>O(1)操作,无序"]
Q2 -- 是 --> LinkedHashSet["LinkedHashSet<br>O(1)操作,保持插入顺序"]
Q1 -- 是 --> Q3{"是否需要并发访问?"}
Q3 -- 是 --> SkipList["ConcurrentSkipListSet<br>线程安全,跳表实现"]
Q3 -- 否 --> TreeSet["TreeSet<br>红黑树实现,O(log n)"]
决策图说明:
- 第一层决策:是否需要排序。若无排序需求,进入第二层:是否需要保持插入顺序。否→
HashSet;是→LinkedHashSet。 - 若有排序需求:进入第三层:是否需要并发访问。是→选择线程安全的
ConcurrentSkipListSet;否→选择TreeSet。 - 关键结论:TreeSet 适用于需要元素排序的去重集合,例如排行榜、区间查询、按字典序输出等场景。反例场景:仅需快速去重且不关心顺序时,
HashSet的 O(1) 更具优势;需要插入顺序时使用LinkedHashSet;在高并发排序场景下,应使用ConcurrentSkipListSet。
模块 2:接口与继承体系
TreeSet 的继承体系展现了其作为有序集合的完整能力。下面是其类图:
classDiagram
class Iterable {
<<interface>>
}
class Collection {
<<interface>>
}
class Set {
<<interface>>
}
class SortedSet {
<<interface>>
+comparator()
+first()
+last()
+subSet()
+headSet()
+tailSet()
}
class NavigableSet {
<<interface>>
+lower()
+floor()
+ceiling()
+higher()
+pollFirst()
+pollLast()
+descendingSet()
}
class AbstractSet {
<<abstract>>
}
class TreeSet {
-NavigableMap m
-Object PRESENT
+add(E e)
+remove(Object o)
+contains(Object o)
+lower(E e)
...
}
class TreeMap {
-Comparator comparator
-Entry root
+put(K key, V value)
+remove(Object key)
...
}
class Cloneable {
<<interface>>
}
class Serializable {
<<interface>>
}
Iterable <|-- Collection
Collection <|-- Set
Set <|-- SortedSet
SortedSet <|-- NavigableSet
AbstractSet ..|> Set
AbstractSet <|-- TreeSet
TreeSet ..|> NavigableSet
TreeSet ..|> Cloneable
TreeSet ..|> Serializable
TreeSet *-- TreeMap : m
类图说明:
- 第一层接口继承:
Iterable→Collection→Set→SortedSet→NavigableSet。SortedSet赋予了排序能力(subSet、headSet、tailSet、first、last),NavigableSet进一步扩展了导航方法(lower、floor、ceiling、higher、pollFirst、pollLast、descendingSet)。 - 抽象类支持:
TreeSet继承AbstractSet,获得了Set接口的大部分模版实现,减少编码量。 - 内部组合关系:
TreeSet持有一个NavigableMap字段m,其实际类型为TreeMap。这表明 TreeSet 的所有操作均委托给TreeMap实例,元素作为键存储。 - Cloneable 和 Serializable:TreeSet 支持浅拷贝和序列化,底层依赖
TreeMap的相应实现。
Part 2:存储与构造篇
模块 3:存储结构与底层依赖(源码剖析)
核心字段
打开 JDK 8 TreeSet 源码,其核心字段定义如下(简化):
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
// 构造器...
}
m:实际存储元素的NavigableMap,构造函数中会被赋予一个TreeMap实例。TreeMap内部使用红黑树作为数据结构。PRESENT:一个共享的哑元对象。因为TreeMap是键值对存储,而Set只需存储键,所以所有元素作为键,值统一为PRESENT,以节约内存。
TreeMap.Entry 结构
TreeMap 内部的节点类型为 TreeMap.Entry<K,V>,它实现了 Map.Entry 并增加了红黑树所需的指针和颜色:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; // 左子节点
Entry<K,V> right; // 右子节点
Entry<K,V> parent; // 父节点
boolean color = BLACK; // 节点颜色(红/黑)
}
这种结构使得 TreeSet 的每个元素以 Entry<E,Object> 的形式存储在红黑树中,其中 key 即为集合元素,value 固定为 PRESENT。
红黑树的五个性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 所有叶子(NIL)是黑色。
- 每个红色节点的两个子节点都是黑色。(即不能有连续的红色节点)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
这些性质保证了没有一条路径会比其他路径长出两倍,从而树接近平衡,操作时间复杂度为 O(log n)。
组合关系图
classDiagram
class TreeSet {
-NavigableMap m
-Object PRESENT
}
class TreeMap {
-Entry root
-int size
-Comparator comparator
+put(K key, V value)
+remove(Object key)
}
class Entry {
K key
V value
Entry left
Entry right
Entry parent
boolean color
}
TreeSet *-- TreeMap : m (组合)
TreeMap *-- Entry : root (红黑树节点)
Entry -- Entry : left/right/parent
结构说明:
- 组合关系:
TreeSet与TreeMap是强组合关系,TreeSet的实例化必定会创建一个TreeMap实例赋给m。 - 节点链接:
TreeMap通过root字段持有整棵红黑树的根节点,每个Entry通过left、right、parent链接父子节点,形成二叉搜索树结构。 - 颜色维护:
Entry.color字段支持红黑树性质,确保插入与删除后通过旋转和变色维持平衡。 - 数据映射:集合元素作为
Entry.key,值统一为TreeSet.PRESENT。这个映射将Set语义映射到了Map存储模型上。
模块 4:构造方法(源码剖析)
TreeSet 提供了四个构造函数,均通过创建 TreeMap 实例来初始化底层的 m:
-
无参构造 – 自然排序
public TreeSet() { this(new TreeMap<>()); }底层
TreeMap使用自然排序,即要求元素必须实现Comparable接口。 -
指定比较器
public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }当元素未实现
Comparable或需要自定义顺序时,传入Comparator。Comparator 的优先级高于元素自身的 Comparable。 -
从普通集合构造
public TreeSet(Collection<? extends E> c) { this(); addAll(c); }先创建自然排序的
TreeMap,然后调用addAll批量添加。若c的元素未实现Comparable,会在add时抛出ClassCastException。 -
从有序集合构造
public TreeSet(SortedSet<E> s) { this(s.comparator()); addAll(s); }此构造器保留原
SortedSet的比较器,并按原顺序批量插入,可高效复制有序集合并维护相同排序规则。
Demo 代码:不同构造方式演示
import java.util.*;
public class TreeSetConstructDemo {
public static void main(String[] args) {
// 1. 自然排序(Integer实现Comparable)
TreeSet<Integer> set1 = new TreeSet<>();
Collections.addAll(set1, 5, 2, 8);
System.out.println(set1); // [2, 5, 8]
// 2. 自定义比较器(降序)
TreeSet<Integer> set2 = new TreeSet<>(Comparator.reverseOrder());
Collections.addAll(set2, 5, 2, 8);
System.out.println(set2); // [8, 5, 2]
// 3. 从Collection构造(元素必须可比较)
List<String> list = Arrays.asList("banana", "apple", "cherry");
TreeSet<String> set3 = new TreeSet<>(list);
System.out.println(set3); // [apple, banana, cherry]
// 4. 从SortedSet构造(保留比较器)
SortedSet<String> sorted = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
sorted.add("Apple");
sorted.add("banana");
TreeSet<String> set4 = new TreeSet<>(sorted);
set4.add("APPLE"); // 忽略,因为大小写不敏感比较相同
System.out.println(set4); // [Apple, banana]
}
}
Part 3:核心原理篇
模块 5:add 操作——有序插入与去重(源码剖析)
TreeSet 的 add 方法直接委托给内部 TreeMap 的 put:
// TreeSet.java
public boolean add(E e) {
return m.put(e, PRESENT) == null; // 若键不存在则返回null代表新增成功
}
TreeMap.put 核心流程
TreeMap.put 方法执行以下步骤(基于 JDK 8 简化):
- 若根节点
root == null,直接创建新节点并设为根,着色为黑,完成。 - 否则,从根开始沿红黑树向下搜索:
- 通过
comparator(若存在)或元素自身的Comparable进行比较。 - 若
cmp < 0,进入左子树;cmp > 0,进入右子树。 - 若
cmp == 0,表示键相等,则用新值覆盖旧值,并返回旧值(TreeSet 层判断为重复,add返回 false)。
- 通过
- 找到插入位置(叶子),创建新
Entry并链接到父节点,默认为红色。 - 调用
fixAfterInsertion(e)修复红黑树性质(可能触发旋转和变色)。
fixAfterInsertion 自平衡简述:
- 插入节点 X 为红色。循环处理直到 X 成为根或父节点为黑。
- Case 1:X 的叔叔节点为红色 → 父与叔变黑,祖父变红,X 指向祖父继续循环。
- Case 2:X 的叔叔为黑色,且 X 为内侧孙子 → 通过旋转将 X 转为外侧情形。
- Case 3:X 的叔叔为黑色,且 X 为外侧孙子 → 父变黑,祖父变红,对祖父旋转。
- 最终根节点强制涂黑。
add 操作调用链与自平衡流程图
sequenceDiagram
participant Client
participant TreeSet
participant TreeMap
participant RedBlackTree
Client->>TreeSet: add(element)
TreeSet->>TreeMap: put(element, PRESENT)
alt 树为空
TreeMap->>RedBlackTree: 创建黑色根节点
TreeMap-->>TreeSet: null (新增成功)
else 树非空
TreeMap->>RedBlackTree: 从根比较查找插入位置
loop 二叉查找
RedBlackTree->>RedBlackTree: compare(element, current.key)
alt cmp == 0
RedBlackTree->>RedBlackTree: 更新value, 返回旧值
TreeMap-->>TreeSet: 旧值 (add返回false)
else cmp < 0 或 > 0
RedBlackTree->>RedBlackTree: 移向左/右子节点
end
end
RedBlackTree->>RedBlackTree: 创建红色节点并链接
RedBlackTree->>RedBlackTree: fixAfterInsertion(新节点)
loop 直到根或父为黑
RedBlackTree->>RedBlackTree: 检查父/叔颜色
alt 叔叔红色
RedBlackTree->>RedBlackTree: 父叔黑,祖父红,上溯
else 叔叔黑色
alt 内侧孙子
RedBlackTree->>RedBlackTree: 旋转父节点
end
RedBlackTree->>RedBlackTree: 父黑,祖父红,旋转祖父
end
end
RedBlackTree->>RedBlackTree: 根节点设为黑色
TreeMap-->>TreeSet: null (新增成功)
end
TreeSet-->>Client: true/false
流程图说明:
- 第一层 – 调用委托:
TreeSet.add(e)将操作完全委托给TreeMap.put(e, PRESENT),返回值决定是否新增成功。 - 第二层 – 二叉查找:
TreeMap.put从根开始比较,利用比较器/Comparable确定方向。若遇到cmp == 0的节点,直接替换旧值并返回旧值,此时add返回false(去重)。这是 TreeSet 基于比较判等的核心体现。 - 第三层 – 新节点插入:若查找至叶子未发现相同键,则在该处创建红色新节点并链接到父节点。选择红色是为了尽量不破坏性质 5(黑高),减少修复复杂度。
- 第四层 – 自平衡修复:
fixAfterInsertion循环处理可能违反的性质(连续红节点)。**Case 1(叔红)**通过变色上推问题;**Case 2/3(叔黑)**通过旋转解决。关键结论:插入后至多两次旋转即可恢复平衡,每次修复保证 O(log n) 时间。 - 终点:无论修复如何,最终将根节点设为黑色,确保性质 2 成立。此时
put返回null,add返回true。
Demo:add 与重复判断
TreeSet<Person> set = new TreeSet<>(Comparator.comparingInt(Person::getAge));
set.add(new Person("Alice", 30));
set.add(new Person("Bob", 30)); // compare结果为0,判定重复,插入失败
System.out.println(set.size()); // 1
模块 6:remove 与 contains 操作
remove 源码
public boolean remove(Object o) {
return m.remove(o) == PRESENT;
}
TreeMap.remove(key) 的核心流程:
- 调用
getEntry(key)利用二叉查找定位节点。 - 若找到,调用
deleteEntry(p)删除节点并调用fixAfterDeletion(x)修复平衡。 - 删除节点时根据其子节点数目分三种情况:
- 无子节点:直接删除。
- 只有一个子节点:用子节点替代。
- 有两个子节点:找到后继节点(右子树最小节点),拷贝键值,转为删除后继节点(前两种情况)。
- 若删除的节点为黑色,必须调用
fixAfterDeletion调整黑高平衡(可能涉及兄弟节点借黑或变色旋转)。
contains 源码
public boolean contains(Object o) {
return m.containsKey(o);
}
TreeMap.containsKey 直接调用 getEntry(key) 进行纯查找,不修改树结构,时间复杂度 O(log n)。
remove 调用链流程图
flowchart TD
A["TreeSet.remove(o)"] --> B["TreeMap.remove(o)"]
B --> C{"getEntry(o) 查找节点"}
C -->|"找不到"| D["返回 null remove返回false"]
C -->|"找到节点p"| E{"判断p子节点数"}
E -->|"无子节点"| F["直接解除链接"]
E -->|"一个子节点"| G["用该子节点替换p"]
E -->|"两个子节点"| H["找到后继节点succ (右子树最小节点)"]
H --> I["拷贝succ键值到p"]
I --> J["转变为删除succ (子节点数≤1)"]
J --> F
F --> K{"p的颜色?"}
G --> K
K -->|"红色"| L["无需调整 remove返回PRESENT"]
K -->|"黑色"| M["fixAfterDeletion(replacement)"]
M --> L
流程图说明:
- 第一层:查找。
getEntry基于比较器/Comparable 进行二分定位,若未找到直接返回null,remove返回false。 - 第二层:删除逻辑。根据待删节点
p的子节点数量分类处理。双子女情况通过寻找后继(右子树最左节点)转换为删除至多一个子节点的问题,巧妙复用单子逻辑。 - 第三层:颜色修复。关键结论:删除红色节点不影响黑高,无需调整;删除黑色节点会破坏性质 5,需要调用
fixAfterDeletion,通过兄弟节点的颜色与子节点情况,执行借黑(旋转+变色)操作,确保树重新平衡。contains操作完全不经过修改路径,仅有查找过程。
模块 7:导航方法与范围视图(源码剖析)
TreeSet 的导航能力来自 NavigableSet 接口,内部全部依赖于 TreeMap 的对应实现。
导航方法源码映射(以 lower 为例)
// TreeSet.java
public E lower(E e) {
return m.lowerKey(e);
}
TreeMap.lowerKey 的实现核心是 getLowerEntry(key) 方法,它在红黑树中进行二分定位,寻找严格小于 key 的最大节点。同理,floor、ceiling、higher 分别对应 getFloorEntry、getCeilingEntry、getHigherEntry。
导航方法查找示意图(lower):
flowchart TD
Start["开始 lower(e)"] --> Finder["从根节点开始"]
Finder --> Loop{"节点t != null?"}
Loop -- 否 --> Return["返回 lastResult"]
Loop -- 是 --> Compare{"compare(e, t.key)"}
Compare -- "> 0 (e > t.key)" --> Record["记录t为候选<br>lastResult = t.key"]
Record --> GoRight["t = t.right"]
GoRight --> Loop
Compare -- "<= 0" --> GoLeft["t = t.left"]
GoLeft --> Loop
流程图说明:
- 数据结构映射:利用二叉搜索树性质,当
e大于当前节点键时,当前节点成为候选答案(因为它小于e),然后进入右子树寻找可能更大的候选;当e小于等于当前键时,直接进入左子树寻找更小的节点。遍历结束后lastResult即为严格小于e的最大键。 - 方法对应:
TreeMap.getLowerEntry忠实执行上述逻辑,floor等仅有细微差异(比较条件调整为包含等号)。 - 时间复杂度:O(log n),得益于红黑树的平衡高度。
范围视图(subSet/headSet/tailSet)
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}
范围视图返回一个新的 TreeSet,但其内部的 m 是原 TreeMap 的子视图(SubMap)。这意味着视图与原集合共享同一底层树结构,一方的结构性修改(添加/删除)会反映到另一方,并受视图区间边界的限制。
Part 4:迭代与序列化篇
模块 8:迭代器与有序遍历
迭代器实现
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
实际上,TreeMap 的 KeyIterator 基于中序遍历实现。中序遍历的顺序为:左子树 → 根 → 右子树,这正是红黑树提供升序遍历的关键。
中序遍历迭代器原理:
stateDiagram-v2
[*] --> 未初始化
未初始化 --> 压栈最左路径: 初始化
压栈最左路径 --> 弹出栈顶节点: next()
弹出栈顶节点 --> 转向右子: 右子存在?
转向右子 --> 压栈右子的最左路径: 是
压栈右子的最左路径 --> 弹出栈顶节点
转向右子 --> 弹出栈顶节点: 否,且栈非空
弹出栈顶节点 --> [*]: 栈空且无右子
状态图说明:
- 初始化阶段:迭代器创建时,从根开始将最左路径上的所有节点压入栈。栈顶即是最小元素。
- 迭代推进:每次调用
next(),弹出栈顶节点并返回;随后转向该节点的右子树,并将右子树的最左路径压栈。该过程严格遵循左-根-右的中序顺序,从而保证了升序输出。 - 降序迭代器
descendingIterator()则采用对称的右-根-左顺序,或利用descendingMap().keySet().iterator()实现。 - fail-fast 机制:
TreeMap内部维护modCount,迭代器创建时记录expectedModCount,每次访问前检查是否被并发修改,若不一致立即抛出ConcurrentModificationException。
模块 9:序列化与克隆
- 序列化:
TreeSet实现了Serializable。writeObject方法委托内部TreeMap的writeObject保存比较器、容量和所有键(值不保存,反序列化时重建PRESENT)。readObject同理恢复树结构。 - 克隆:
clone()方法调用super.clone()后,对内部TreeMap进行浅拷贝(TreeMap.clone()),因此新旧集合共享相同的元素引用,但树结构独立。
Part 5:对比与陷阱篇
模块 10:TreeSet vs HashSet vs LinkedHashSet——全维对比
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层结构 | HashMap (哈希表) | LinkedHashMap (哈希表+双向链表) | TreeMap (红黑树) |
| 元素顺序 | 无序 | 保持插入顺序 | 按排序规则(自然/比较器) |
| 判重逻辑 | hashCode + equals | hashCode + equals | compareTo / compare 返回 0 |
| 基本操作时间复杂度 | O(1) 平均 | O(1) 平均 | O(log n) |
| null 元素 | 允许一个 null | 允许一个 null | 通常不允许(取决于比较器) |
| 导航/范围操作 | 无 | 无 | lower、floor、subSet 等 |
| 迭代性能 | 与容量正比 | 与size正比 | 与size正比,有序 |
| 内存开销 | 较低 | 中等(额外链表) | 较高(节点含颜色、指针) |
| 适用场景 | 快速去重,无序 | 需要插入顺序的去重 | 需要排序、范围查询、有序遍历 |
选型决策树(同模块1)已给出,关键结论: 如果仅需快速去重不关心顺序,HashSet 是首选;需要记录插入顺序则用 LinkedHashSet;只有在需要排序或导航操作时,才应选择 TreeSet。使用 TreeSet 必须重视比较器与 equals 的一致性,否则可能破坏 Set 的语义。
模块 11:TreeSet vs ConcurrentSkipListSet
两者均实现 NavigableSet,API 高度兼容,但内部实现与并发行为截然不同。
- 数据结构:
ConcurrentSkipListSet基于跳表(Skip List) 实现,通过多层索引支持快速查找;TreeSet 基于红黑树。 - 线程安全:
ConcurrentSkipListSet是线程安全的,支持高并发下的无锁/轻量级锁操作;TreeSet 非线程安全,并发修改将导致不可预知的结果。 - 性能:单线程下红黑树常数因子稍优于跳表,但跳表更易于实现非阻塞并发。在需要并发有序遍历或频繁并发修改时,应使用
ConcurrentSkipListSet。 - null 元素:两者都不允许
null(除非比较器支持,但跳表实现也规范禁止)。选择时主要依据并发需求。
模块 12:常见陷阱与最佳实践
陷阱 1:未实现 Comparable 且未传 Comparator
TreeSet<Object> set = new TreeSet<>();
set.add(new Object()); // 抛出 ClassCastException
修复:实现 Comparable 或提供 Comparator。
陷阱 2:比较器与 equals 不一致
假设 Person 的 equals 基于身份证,而 Comparator 仅基于姓名。
TreeSet<Person> set = new TreeSet<>(Comparator.comparing(Person::getName));
Person p1 = new Person("Alice", "ID001");
Person p2 = new Person("Alice", "ID002");
set.add(p1);
set.add(p2); // compare返回0,视为重复,但equals不等
后果:Set 语义被破坏,p1 和 p2 本该视为不同元素却只存其一。最佳实践:确保 compareTo 或 Comparator 与 equals 定义一致。
陷阱 3:存入元素后修改比较字段
TreeSet<Person> set = new TreeSet<>(Comparator.comparing(Person::getAge));
Person p = new Person("Bob", 25);
set.add(p);
set.add(new Person("Alice", 30));
p.setAge(40); // 修改导致排序失效,无法正常查找或删除
原理:红黑树的排序基于插入时键的属性,后续改变不会触发树的重排,导致结构混乱。最佳实践:存入 TreeSet 的元素最好为不可变对象(如 String、LocalDate)。
陷阱 4:并发修改
多线程同时修改 TreeSet 可能抛出 ConcurrentModificationException 或导致数据不一致。解决方案:用 Collections.synchronizedNavigableSet 包装,或使用 ConcurrentSkipListSet。
陷阱 5:子视图与原集合的结构性修改冲突
NavigableSet<Integer> sub = set.subSet(1, true, 10, false);
sub.add(5); // 正常
set.add(0); // 合法但可能影响sub的边界
set.add(8); // 合法,但可能导致sub的某些操作异常
范围视图的边界在违反约束插入时会抛 IllegalArgumentException,但原集合的合法修改不应导致视图失效,但 JDK 源码保证视图的依赖关系,需谨慎同步修改。
Part 6:总结与面试篇
模块 13:注意事项与性能总结
- 时间复杂度:增删查、导航操作均为 O(log n);
first/last因红黑树维护最左最右节点可达 O(1)。 - 空间开销:每个元素存储为一个
TreeMap.Entry,额外包含left、right、parent、color四个引用和一个布尔值,空间消耗高于HashMap的Node。 - 元素约束:鼓励使用不可变对象作为元素,避免比较键可变带来的问题。
- 比较器优先:构造时传入
Comparator可覆盖自然顺序,且允许排序对象未实现Comparable。 - 视图时效性:
subSet等视图是实时反应原集合变化的,共享底层数据。
模块 14:面试高频专题
1. TreeSet 的底层数据结构是什么?为什么能保证有序?
标准回答:底层基于 TreeMap,使用红黑树存储。红黑树是自平衡的二叉搜索树,通过维护键的比较顺序和自平衡性质,保证中序遍历为升序,从而实现有序性。
追问:如何实现去重?
回答:通过 compareTo 或 compare 返回 0 判定为相同元素,TreeMap.put 会替换旧值而键不变,TreeSet 据此返回 false。
加分回答:可提及红黑树的五个性质及旋转修复,以及 NavigableSet 接口扩展的导航能力。
2. TreeSet 如何判断元素是否重复?与 HashSet 有何不同?
标准回答:TreeSet 依赖 compareTo/compare 返回值 0 判重,HashSet 依赖 hashCode 和 equals。因此如果一个类重写 equals 但 Comparable 按不同逻辑比较,TreeSet 可能出现语义矛盾。
追问:当一个类同时实现了 Comparable 和 equals,但逻辑不一致,TreeSet 会怎样?
加分回答:会破坏 Set 契约(Set 内部不应包含 equals 为 true 的两个元素,但可能因 compare 返回非 0 而同时存在)。
3. 什么是红黑树?它在 TreeSet 中的作用是什么?
标准回答:红黑树是近似平衡的二叉搜索树,通过节点颜色和旋转保持从根到叶最长路径不超过最短路径的两倍。在 TreeSet 中,它确保增删查的操作时间复杂度为 O(log n),并提供有序迭代。
追问:与 AVL 树相比有何优势?
加分回答:红黑树牺牲了部分严格平衡性,但插入删除时的旋转次数更少(最多三次),在修改频繁的场景综合性能更优。
4. TreeSet 的 add 操作源码流程(追问 fixAfterInsertion)
见模块 5 详细流程图及分析。
5. TreeSet 是否允许 null 元素?为什么?
标准回答:通常不允许。使用自然排序时,null 无法与任何对象比较,会抛 NullPointerException;若提供了能处理 null 的比较器,则可以插入 null,但大多数比较器(包括 Comparator.naturalOrder())不处理 null。工程上推荐不要插入 null。
追问:如果比较器特别处理 null,TreeMap 的 put 会正常插入吗?
加分回答:会,只要比较器能返回有意义的值,TreeMap 就会将 null 作为普通键对待。但后续操作必须确保所有比较都能处理 null,否则仍然抛出异常。
6. 自然排序和比较器排序的区别?何时用哪个?
标准回答:自然排序要求元素实现 Comparable,排序逻辑与类绑定;比较器排序通过 Comparator 外部定义,不修改原类,且可提供多种排序策略。当类本身具备自然顺序且对顺序无特殊要求时用自然排序;需要降序、特殊规则或类未实现 Comparable 时用比较器。
追问:两者同时存在时哪个起作用?
加分回答:比较器优先级高于自然排序。
7. TreeSet 和 HashSet 的性能对比?如何选择?
标准回答:HashSet O(1),TreeSet O(log n);HashSet 无序,TreeSet 有序。需要排序、范围查询时选 TreeSet;仅需快速去重时选 HashSet。内存开销 TreeSet 更大。可在模块 10 对比表中补充。
8. TreeSet 的导航方法(lower/floor/ceiling/higher)如何使用?
标准回答:lower(e) 返回严格小于 e 的最大元素;floor(e) 返回小于等于 e 的最大元素;ceiling(e) 返回大于等于 e 的最小元素;higher(e) 返回严格大于 e 的最小元素。它们均以 O(log n) 复杂度实现,适用于区间匹配场景。
9. 为什么比较器要与 equals 保持一致?不一致会有什么后果?
标准回答:SortedSet 的接口契约强烈推荐(但不强制)比较器与 equals 一致。如果不一致,SortedSet 的行为虽能遵守自身顺序,但可能违背 Set 接口基于 equals 的约定,例如两个 equals 的对象可能因其 compare 结果不同而同时在集合中,破坏了 Set 唯一性语义。
10. TreeSet 线程安全吗?并发中有什么替代方案?
标准回答:不是。并发环境可用 Collections.synchronizedNavigableSet(new TreeSet<>()) 进行同步包装,或直接使用 ConcurrentSkipListSet,后者基于跳表提供更好的并发伸缩性。
追问:synchronizedNavigableSet 和 ConcurrentSkipListSet 的区别?
加分回答:前者为所有方法加锁,迭代需外部同步;后者无锁或轻量级锁,迭代器弱一致性,不抛 ConcurrentModificationException。
11. 修改 TreeSet 中元素的比较字段会发生什么?如何避免?
标准回答:修改会影响红黑树的有序结构,但树不自动重排,导致查找、删除可能失败,迭代顺序错乱。避免方法:只在 TreeSet 中存储不可变对象,或保证修改字段不参与比较器逻辑;若必须修改,先移除,修改后再重新加入。