“为什么 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 采取两阶段判断:

  1. hashCode → 快速定位桶(O(1)效率)
  2. 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 进来时:

  1. 通过 hashCode 判断应该放到哪个桶
  2. 在这个桶里通过 equals 判断是否与已有元素相等
  3. 相等 → 不放
  4. 不相等 → 追加到链表或红黑树

用一句话总结:

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 这种:

“高性能 + 不重复”的强大数据结构。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐