HashSet 去重实现原理
HashSet 的去重能力依赖于 哈希表(HashMap) 和 equals()/hashCode() 方法的配合,核心逻辑如下:
1. 底层存储结构
- HashSet 内部使用 HashMap 存储元素(JDK 源码):
private transient HashMap<E, Object> map;
- 元素作为 HashMap 的 Key,Value 统一为静态空对象
PRESENT
(无实际意义)。
- 元素作为 HashMap 的 Key,Value 统一为静态空对象
2. 去重关键逻辑
当调用 add(E e)
方法时,执行以下步骤:
public boolean add(E e) {
return map.put(e, PRESENT) == null; // 若Key不存在,返回true
}
去重流程:
- 计算哈希值
- 调用元素的
hashCode()
方法,确定存储位置(HashMap 的桶位置)。
- 调用元素的
- 检查哈希冲突
- 若目标桶为空,直接存入(无重复)。
- 若桶非空,遍历链表/红黑树,调用
equals()
逐个比较:- 存在相同元素(
equals()
返回true
):放弃插入,返回false
。 - 无相同元素:存入新节点,返回
true
。
- 存在相同元素(
3. 关键方法要求
hashCode()
:- 必须保证相同对象返回相同值(否则无法定位到正确桶)。
- 不同对象尽量返回不同值(减少哈希冲突)。
equals()
:- 必须严格比较对象内容(如
String
比较字符序列)。
- 必须严格比较对象内容(如
示例:String
类的去重
HashSet<String> set = new HashSet<>();
set.add("A"); // 存入 "A"(hashCode=65)
set.add("A"); // 计算相同hashCode,equals()比较为true,拒绝重复
set.add("B"); // 存入 "B"(hashCode=66)
4. 特殊情况处理
- 哈希冲突:
- 不同对象可能计算相同
hashCode
(如"Aa"
和"BB"
的hashCode均为2112)。 - 此时依赖
equals()
进一步判断是否为同一对象。
- 不同对象可能计算相同
- 自定义对象:
- 若未重写
hashCode()
和equals()
,默认使用对象地址比较,可能导致逻辑重复但无法去重。
- 若未重写
错误示例:
class User { String name; }
HashSet<User> set = new HashSet<>();
set.add(new User("Tom")); // 存入对象A
set.add(new User("Tom")); // 存入对象B(未重写方法,地址不同,无法去重)
正确做法:
class User {
String name;
@Override
public int hashCode() { return name.hashCode(); }
@Override
public boolean equals(Object o) { /* 比较name字段 */ }
}
总结
- 去重本质:通过
hashCode()
快速定位 +equals()
精确判等。 - 性能依赖:良好的
hashCode()
设计能减少冲突,提升效率。 - 注意事项:
- 存入自定义对象时,必须正确重写
hashCode()
和equals()
。 - 避免修改已存入 HashSet 的对象的哈希相关字段(会导致内存泄漏)。
- 存入自定义对象时,必须正确重写