一、整体架构图
Set (接口) ├── HashSet │ └── LinkedHashSet ├── TreeSet ├── CopyOnWriteArraySet └── ConcurrentSkipListSet └── (Java 6+)二、详解
1. HashSet
HashSet 就是披着 Set 外衣的 HashMap——它把所有元素当作 HashMap 的 Key,用一个固定的空对象占住 Value 位,借助哈希表的 O(1) 查找实现快速去重。元素必须正确重写 hashCode() 和 equals(),否则去重逻辑会失效。
定位:最基础、最常用的去重集合,追求极致速度。
| 项目 | 内容 |
|---|---|
| 底层 | HashMap<E, Object> |
| 有序性 | ❌ 完全无序,迭代顺序不可预测 |
| null 支持 | ✅ 允许一个null |
| 线程安全 | ❌ 非线程安全 |
| 时间复杂度 | O(1) — add/remove/contains |
| 空间开销 | 较小,仅存储 Key + 一个固定空对象 |
核心源码:
publicclassHashSet<E>extendsAbstractSet<E>implementsSet<E>{privatetransientHashMap<E,Object>map;privatestaticfinalObjectPRESENT=newObject();// 所有 Value 都是它publicbooleanadd(Ee){returnmap.put(e,PRESENT)==null;// 返回 null 说明是新增}}本质:HashSet = HashMap 的 Key 集合。
去重原理
hashCode() 定位桶 → equals() 比较桶内元素 → 相同则覆盖,不同则链入。
1. 计算 hashCode() → 定位桶位置 2. 桶内无元素 → 直接插入 3. 桶内有元素 → 用 equals() 逐个比较 - equals 返回 true → 认为是重复,不插入,返回旧值 - 全部不相等 → 链入桶内(JDK8+ 可能转红黑树)所以:元素必须正确重写 hashCode() 和 equals(),否则去重失效。
扩容机制
完全继承 HashMap 的规则:
- 默认初始容量:16
- 负载因子:0.75
- 扩容时机:元素数 > 容量 × 0.75
- 扩容方式:容量翻倍,所有元素 rehash 重新分布
2. LinkedHashSet
定位:HashSet 的"有序升级版",保留插入顺序。
| 项目 | 内容 |
|---|---|
| 底层 | LinkedHashMap<E, Object> |
| 有序性 | ✅插入有序(按添加顺序迭代) |
| null 支持 | ✅ 允许一个null |
| 线程安全 | ❌ 非线程安全 |
| 时间复杂度 | O(1) |
| 空间开销 | 略大于 HashSet,额外维护双向链表 |
核心机制:
在 HashMap 的每个节点上额外挂了两个指针 before / after,形成一条按插入顺序链接的双向链表。
// LinkedHashMap.Entry 继承 HashMap.Node,多了两个指针staticclassEntry<K,V>extendsHashMap.Node<K,V>{Entry<K,V>before,after;// 双向链表指针}迭代时:遍历这条双向链表,而非哈希桶数组,因此顺序稳定。
适用场景:需要按添加顺序去重,如配置项加载、历史记录去重。
3. TreeSet
定位:有序集合,元素自动排序,支持范围查询。
| 项目 | 内容 |
|---|---|
| 底层 | TreeMap<E, Object>(红黑树) |
| 有序性 | ✅自然排序或自定义比较器排序 |
| null 支持 | ❌ 不允许null(无法比较) |
| 线程安全 | ❌ 非线程安全 |
| 时间复杂度 | O(log n) — add/remove/contains |
| 空间开销 | 较大,每个节点存左右子树指针、颜色标记 |
核心机制:
publicclassTreeSet<E>extendsAbstractSet<E>implementsNavigableSet<E>{privatetransientNavigableMap<E,Object>m;publicbooleanadd(Ee){returnm.put(e,PRESENT)==null;}}底层是红黑树(自平衡二叉搜索树),元素按比较规则排列:
- 元素实现 Comparable 接口(自然排序)
- 或构造时传入 Comparator(自定义排序)
特有 API:
Elower(Ee);// 严格小于 e 的最大元素Efloor(Ee);// 小于等于 e 的最大元素Eceiling(Ee);// 大于等于 e 的最小元素Ehigher(Ee);// 严格大于 e 的最小元素SortedSet<E>subSet(Efrom,Eto);// 范围子集适用场景:需要排序、范围查询、排行榜、区间检索。
4. CopyOnWriteArraySet
定位:线程安全的"读多写少"集合,写操作复制整个数组。
| 项目 | 内容 |
|---|---|
| 底层 | CopyOnWriteArrayList<E>(数组) |
| 有序性 | ✅ 插入有序(按数组索引) |
| null 支持 | ✅ 允许null |
| 线程安全 | ✅ 线程安全(写时复制) |
| 时间复杂度 | 读 O(1),写 O(n) |
| 空间开销 | 写操作时翻倍(复制新数组) |
核心机制:
publicclassCopyOnWriteArraySet<E>extendsAbstractSet<E>{privatefinalCopyOnWriteArrayList<E>al;publicbooleanadd(Ee){returnal.addIfAbsent(e);// 先遍历检查是否存在,不存在则复制数组添加}}写时复制(COW):
- 读操作:直接读当前数组,无锁,极快
- 写操作:加锁 → 复制新数组 → 修改新数组 → 替换引用
注意:add() 时先用 indexOf 遍历检查是否已存在(O(n)),再复制数组插入。所以去重效率不高,数据量大时慎用。
适用场景:事件监听器列表、配置项集合——读极多、写极少、遍历频繁。
5. ConcurrentSkipListSet
定位:线程安全的有序集合,高并发下的 TreeSet 替代品
| 项目 | 内容 |
|---|---|
| 底层 | ConcurrentSkipListMap<E, Object>(跳表) |
| 有序性 | ✅ 自然排序或自定义排序 |
| null 支持 | ❌ 不允许null |
| 线程安全 | ✅ 线程安全(CAS + 细粒度锁) |
| 时间复杂度 | O(log n) |
| 空间开销 | 较大,跳表多层索引 |
核心机制:
底层是 **跳表(**Skip List)——一种用概率平衡替代严格旋转的平衡数据结构:
Level 3: 1 -------------------------> 9 Level 2: 1 ---------> 5 ---------> 9 Level 1: 1 -> 3 -> 5 -> 7 -> 9- 无锁读,CAS 写
- 并发性能优于 TreeSet + synchronized
- 支持 NavigableSet 全部范围查询 API
适用场景:高并发下需要排序、范围查询,替代 Collections.synchronizedSortedSet(new TreeSet())。
三、对比总表
| 特性 | HashSet | LinkedHashSet | TreeSet | CopyOnWriteArraySet | ConcurrentSkipListSet |
|---|---|---|---|---|---|
| 底层结构 | HashMap | LinkedHashMap | TreeMap(红黑树) | CopyOnWriteArrayList(数组) | ConcurrentSkipListMap(跳表) |
| 有序性 | ❌ 无序 | ✅ 插入有序 | ✅ 排序有序 | ✅ 插入有序 | ✅ 排序有序 |
| null 元素 | ✅ 1个 | ✅ 1个 | ❌ 不允许 | ✅ 允许 | ❌ 不允许 |
| 线程安全 | ❌ | ❌ | ❌ | ✅ COW | ✅ CAS |
| add 复杂度 | O(1) | O(1) | O(log n) | O(n) | O(log n) |
| contains 复杂度 | O(1) | O(1) | O(log n) | O(n) | O(log n) |
| 遍历性能 | 与容量相关 | 与容量相关 | 与元素数相关 | 极快(快照) | 与元素数相关 |
| 内存开销 | 小 | 中(双向链表) | 大(树节点) | 写时翻倍 | 大(多层索引) |
| 迭代器 | fail-fast | fail-fast | fail-fast | 快照(弱一致) | 弱一致 |
| 比较依据 | hashCode+equals | hashCode+equals | Comparable/Comparator | equals | Comparable/Comparator |
四、原理深度对比
| 集合 | 去重方式 | 关键点 |
|---|---|---|
| HashSet | hashCode()→ 桶定位 →equals()比较 | 哈希冲突用链表/红黑树解决 |
| LinkedHashSet | 同 HashSet | 额外维护插入顺序链表,不影响去重 |
| TreeSet | compareTo()/compare()比较 | 比较结果为 0 即视为重复 |
| CopyOnWriteArraySet | equals()遍历数组查找 | 线性扫描,效率低 |
| ConcurrentSkipListSet | 跳表索引定位 →compareTo()比较 | CAS 保证并发安全 |
重要陷阱
TreeSet 的 “相等” 陷阱:
TreeSet<Person>set=newTreeSet<>((a,b)->a.age-b.age);// 如果两个人 age 相同,compare 返回 0,TreeSet 认为它们是同一个元素!// 即使 name 不同,第二个也会被去重掉CopyOnWriteArraySet 的性能陷阱:
// 每次 add 都要 O(n) 扫描 + 数组复制,数据量大时极慢CopyOnWriteArraySet<Integer>set=newCopyOnWriteArraySet<>();for(inti=0;i<10000;i++){set.add(i);// 越来越慢!}五、选择决策树
需要线程安全? ├── 是 │ ├── 需要排序/范围查询? → ConcurrentSkipListSet │ └── 读多写少、数据量小? → CopyOnWriteArraySet └── 否 ├── 需要排序/范围查询? → TreeSet ├── 需要保留插入顺序? → LinkedHashSet └── 只追求最快去重? → HashSet六、总结
| 集合 | 一句话 |
|---|---|
| HashSet | 最快的去重桶,无序,O(1) |
| LinkedHashSet | 有记忆的去重桶,记住你来过的顺序 |
| TreeSet | 会自动排队的去重桶,支持"第几名到第几名" |
| CopyOnWriteArraySet | 写一次复制全班的去重桶,读飞快、写巨慢 |
| ConcurrentSkipListSet | 多人同时排队的去重桶,并发安全还能查排名 |