Set 如何保证元素不重复的?
Set 如何保证元素不重复的?
zhangchangsheng面试考察点
- 底层实现理解:面试官不仅仅是想知道 “Set 不重复” 这个结论,更是想知道你是否了解
HashSet底层是用HashMap实现的这一经典设计。 - 判断机制掌握:考察你是否理解判断元素重复的完整逻辑:先比
hashCode(),再比equals(),以及为什么要这样设计。 - 不同实现对比:是否了解
HashSet、TreeSet、LinkedHashSet保证不重复的不同机制。
核心答案
以最常用的 HashSet 为例,它底层基于 HashMap 实现,元素作为 HashMap 的 key 存储,利用 key 的唯一性来保证元素不重复:
| Set 实现类 | 底层结构 | 判重机制 | 特点 |
|---|---|---|---|
| HashSet | HashMap | hashCode() + equals() | 无序,查询 O(1) |
| LinkedHashSet | LinkedHashMap | hashCode() + equals() | 保持插入顺序 |
| TreeSet | TreeMap | compareTo() 或 Comparator | 有序,查询 O(log n) |
一句话总结:HashSet 利用 HashMap 的 key 唯一性,通过 “hashCode 相同 + equals 返回 true” 判断重复,重复元素不会被插入。
深度解析
一、HashSet 底层结构
flowchart TB
A["HashSet 内部持有一个 HashMap<br/>private transient HashMap map<br/>private static final Object PRESENT = new Object()"] --> B["HashSet 中的元素<br/>apple / banana / cherry"]
B -->|"等价于"| C["HashMap 中的存储<br/>Key = 元素,Value = PRESENT"]
C --> D["所有 value 都是同一个哑对象<br/>Set 不关心 Value,用 PRESENT 占位"]
D --> E["核心结论:HashSet 本质上是 HashMap 的 Key 视图"]
上图揭示了 HashSet 的核心设计。关键点:
- 组合而非继承:HashSet 内部持有一个
HashMap实例,所有操作都委托给它 - 哑对象 PRESENT:因为 HashMap 需要 key-value 对,但 Set 只需要 key,所以 value 用一个共享的
Object占位 - 设计模式:这是 “装饰器模式” 或 “委托模式” 的典型应用
二、add() 方法判重流程
flowchart TB
S["源码:return map.put(e, PRESENT) == null"] --> A["1. 调用 map.put(e, PRESENT)"]
A --> B["2. 计算 e.hashCode() 并扰动<br/>h ^ (h >>> 16)"]
B --> C["3. 定位桶:(n - 1) & hash"]
C --> D{"4. 桶为空?"}
D -->|"是"| T["插入成功,返回 true"]
D -->|"否"| E["5. 遍历桶内元素,比较:<br/>hash 值是否相同<br/>e.equals(oldE) 是否返回 true"]
E -->|"相同"| F["覆盖旧值<br/>返回 false(重复元素)"]
E -->|"不相同"| G["插入到链表 / 红黑树尾部<br/>返回 true(插入成功)"]
上图展示了 add() 方法的完整判重流程。核心逻辑:
- 返回值含义:
true:元素不重复,插入成功false:元素已存在,插入失败(但不会报错)
- 两步判断:
- 先比较
hashCode():快速筛选,hash 不同则一定不重复 - 再调用
equals():hash 相同时,确认是否真的相等
- 先比较
- 为什么两步?
hashCode()是整数比较,速度快;equals()可能涉及复杂逻辑,放后面减少调用。
三、核心源码解析
// ==================== HashSet 源码 ==================== |
四、TreeSet 的不同机制
flowchart TB
A["TreeSet 底层是 TreeMap<br/>使用红黑树存储"] -.-> N["注意:TreeSet 不依赖 hashCode() 和 equals()<br/>compareTo 返回 0 的元素,equals 也应返回 true<br/>否则两种 Set 判重结果可能不一致"]
A --> B{"比较大 a 和 b:<br/>有自定义 Comparator?"}
B -->|"有"| C["调用 comparator.compare(a, b)"]
B -->|"没有"| D["调用 a.compareTo(b)<br/>要求元素实现 Comparable 接口"]
C --> E{"比较结果"}
D --> E
E -->|"返回 0"| F["相等(重复),不插入"]
E -->|"返回 小于 0"| G["放左边(左子树)"]
E -->|"返回 大于 0"| H["放右边(右子树)"]
上图展示了 TreeSet 的不同判重机制。关键点:
- 基于比较器:TreeSet 用
compareTo()或compare()判断相等,而非hashCode()+equals() - 排序特性:TreeSet 的主要目的是排序,元素自动按顺序排列
- 一致性风险:如果
compareTo()返回 0 但equals()返回 false,会导致 Set 语义混乱
五、三种 Set 实现对比
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层结构 | HashMap | LinkedHashMap | TreeMap |
| 判重机制 | hashCode + equals | hashCode + equals | compareTo/compare |
| 是否有序 | ❌ 无序 | ✅ 插入顺序 | ✅ 排序顺序 |
| 查询效率 | O(1) | O(1) | O(log n) |
| 插入效率 | O(1) | O(1) | O(log n) |
| 是否允许 null | ✅ 一个 null | ✅ 一个 null | ❌ 不允许(比较会 NPE) |
| 线程安全 | ❌ 不安全 | ❌ 不安全 | ❌ 不安全 |
// 使用示例 |
面试高频追问
- HashSet 允许添加 null 吗?
- 允许!最多一个 null(因为 HashMap 允许一个 null key)
- null 的 hashCode() 会被特殊处理,返回 0
- 如果只重写 equals() 不重写 hashCode() 会怎样?
- HashSet 判重失效!相同内容的对象可能被当作不同元素
- 因为 hashCode 不同,会进入不同桶,equals() 根本不会被调用
- HashSet 和 ArrayList 的 contains() 性能差异?
HashSet.contains():O(1),基于 hash 直接定位ArrayList.contains():O(n),需要遍历全部元素- 频繁查找时用 HashSet
常见面试变体
- “HashSet 的底层实现原理是什么?”
- “HashSet 如何判断两个元素是否重复?”
- “HashSet、LinkedHashSet、TreeSet 的区别?”
- “为什么重写 equals() 必须重写 hashCode()?”
记忆口诀
HashSet 原理:底层就是 HashMap,元素当 key,PRESENT 当 value。 判重逻辑:先比 hash 快速筛,再比 equals 确认,都相同才算重复。 三种 Set:HashSet 无序最快,LinkedHashSet 保序,TreeSet 排序。
总结
HashSet 保证元素不重复的核心机制是:底层基于 HashMap 实现,元素作为 key 存储,利用 HashMap 的 key 唯一性。判断重复时,先比较 hashCode(),相同再调用 equals(),都返回 true 才认为是重复元素。TreeSet 则不同,使用 compareTo() 或 Comparator 判断相等。记住:Set 元素必须正确实现 hashCode() 和 equals()。
声明:本文转载自小哈的博客 《Set 如何保证元素不重复的?》,版权归原文作者所有。转载请保留本声明与原文链接。
评论
匿名评论隐私政策
WalineTwikoo
