深入理解 Java 集合中的去重机制:为什么同时需要 hashCode 和 equals
文章目录
“为什么 Set 能自动去重?hashCode 和 equals 是如何配合实现的?”
这一点看似简单,但如果真正理解底层实现逻辑,就会发现其背后包含了 Java 集合框架、哈希算法、对象比较机制等核心思想。
很多同学会回答:
Set 底层使用哈希表,通过 hashCode() + equals() 来判断对象是否重复。
但为什么要同时用这两个方法?为什么不能只用 hashCode?
本篇文章将用清晰、直观、带源码思路的方式,一次性讲透这个问题。
一、为什么 Set 需要去重?
我们知道 Set 的基本特性就是:
- 元素不允许重复
- 顺序不保证
- 要求快速查找
其中 HashSet 是最常用的 Set,它的底层实现依赖 HashMap。
你可以把 HashSet 看成一个“只存 key 的 HashMap”。
来看源码(简化版):
public class HashSet<E> implements Set<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
也就是说,HashSet 的核心逻辑就是:
把元素作为 HashMap 的 key 来存储,而 key 天然不允许重复。
所以我们真正要研究的是:
HashMap 是如何判断 key 是否重复的?
答案就是:
先看 hashCode(粗筛),再看 equals(精筛)。
这是 Java 集合非常精妙的设计。
二、为什么不能只用 hashCode?(因为会冲突)
先说结论:
hashCode 不是唯一标识,不同对象可能有相同 hashCode。
例如:
"Aa".hashCode() == "BB".hashCode() // true
所以:
- 仅靠 hashCode 无法确保两个对象相等
- hashCode 一致的对象只是“可能”相等
- 真正相等还必须比对 equals()
hashCode 用来决定对象应该放在哪个桶(bucket),并不用于最后判定相等。
否则会发生严重错误:
两个完全不同的对象会被 Set 认为相同 → 导致数据丢失。
三、那为什么不能只用 equals?(因为太慢)
如果你只用 equals 来比较对象是否重复,意味着什么?
当你插入一个新对象时:
必须跟集合中所有对象逐一 equals 比较。
这将导致:
- 时间复杂度为 O(n)
- 数据越多越慢
- HashSet 完全失去“快速查找”的优势
这与哈希表 O(1) 查找的设计初衷相违背。
因此,Java 采取两阶段判断:
- hashCode → 快速定位桶(O(1)效率)
- equals → 只在冲突的桶中精确判断(很少)
完美兼顾“速度 + 准确性”。
四、hashCode + equals 如何实现 Set 去重?
当往 HashSet 添加元素 e 时:
流程(简化)如下:
1. 计算 hashCode
hash(e) → 决定 e 落到哪个桶(数组下标)
2. 判断该桶是否为空
- 空 → 直接插入
- 不空 → 桶中有链表或红黑树,需要进一步判断是否重复
3. 在桶里遍历节点并比较 equals
如果存在某个节点 node 满足:
node.hash == e.hash &&
node.key.equals(e)
说明是同一个对象,HashSet 就会拒绝插入。
否则:
- 虽然 hash 一样(冲突)
- equals 不相等
→ 允许插入,形成链表结点或红黑树结点
五、用一张图理解(非常直观)
哈希表(HashMap)
-----------------------------------
桶0 | e1 -> e2 -> e3
桶1 |
桶2 | e4
桶3 |
桶4 | e5 -> e6
...
当新元素 e 进来时:
- 通过 hashCode 判断应该放到哪个桶
- 在这个桶里通过 equals 判断是否与已有元素相等
- 相等 → 不放
- 不相等 → 追加到链表或红黑树
用一句话总结:
hashCode 决定桶,equals 决定是否重复。
六、生活类比:机场安检模型
为了说明 hashCode + equals 的搭配关系,我们可以用一个现实类比:
1. hashCode = X 光机快速扫描
- 快速筛查可能有问题的物品
- 速度快,但不够准确
- 只是“粗筛选”
2. equals = 安检人员人工复检
- 对 X 光机提示的对象做人工检查
- 慢,但准确
- 负责“精确判断”
因此:
只用 hashCode → 会误报
只用 equals → 太慢
二者结合 → 又快又准
七、如果不重写 equals 和 hashCode 会怎样?
如果你自定义类:
class User {
String name;
}
然后加入两个内容一样的对象:
set.add(new User("Tom"));
set.add(new User("Tom"));
结果:不会去重!
因为:
- 默认 equals 比较内存地址(不同对象 → false)
- 默认 hashCode 也与内存地址相关(不同对象 → 不同 hash)
解决方式:
一定要同时重写:
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public boolean equals(Object obj) {
User u = (User) obj;
return Objects.equals(this.name, u.name);
}
这时,Set 才会认为它们是同一个对象。
八、总结
hashCode 和 equals 是 Java 集合框架中最重要的基础规则之一。
它们的职责清晰、分工明确:
- hashCode → 用于快速查找,决定元素的存储位置(“粗筛”)
- equals → 用于精准判断两个对象是否相同(“精筛”)
只有 hashCode:可能错误判断相等
只有 equals:性能极差
二者结合,则实现了 HashSet、HashMap 这种:
“高性能 + 不重复”的强大数据结构。
更多推荐
所有评论(0)