跳转至

有了equals为啥需要hashCode方法?

有了 equals 为什么还要 hashCode

这个问题问到了 Java 集合框架的根基。简单说:equals 负责判断两个对象是否“逻辑相等”,hashCode 负责快速定位对象在哈希表里的存储位置。如果只重写 equals 而忽略 hashCode,哈希表相关的集合(HashMapHashSetHashtable)就会行为错乱,出现“明明相等的对象却找不到”的诡异 bug。


🧩 1. 两者的核心分工

方法 核心作用 类比
equals(Object obj) 判断两个对象是否逻辑相等(比如两个 User 的 ID 相同则视为同一个人) 警察查身份证,确认是不是同一个人
hashCode() 返回对象的哈希码,用于在哈希表中确定存储的桶(bucket)位置 图书馆的索书号,告诉你书在哪个书架

关键点:在 HashMap 这类结构中,先用 hashCode 快速定位桶(O(1)),再用 equals 在桶内精确比对。如果 hashCode 没跟上 equals 的逻辑,第一步就定位错了,后面的 equals 根本不会被调用。


📜 2. 契约:必须遵守的规则

Oracle 官方文档明确规定了 equalshashCode 的契约:

  1. 如果两个对象通过 equals(Object) 判定相等,它们的 hashCode 必须相等。

  2. 两个 equals 不等的对象,hashCode 不一定不等(哈希冲突是允许的),但好的算法应尽量分散。

  3. 在同一个程序运行期间,同一个对象的 hashCode 必须保持不变,除非对象中影响 equals 的属性被修改。

违反第 1 条的后果: 假设你定义了一个 User 类,以 id 为相等依据,并正确重写了 equals,但没有重写 hashCode,而是继承了 Object 的默认实现(通常返回对象的内存地址变换值)。那么两个 id 相同的 User 对象虽然 equalstrue,但 hashCode 几乎肯定不同。 当把它们存入 HashMap 时,它们会散列到不同的桶里,导致 map.get(new User(1)) 返回 null,因为找不到对应的桶——哪怕这个 key 与 map 中已有的 key 逻辑相等。


🧪 3. 验证:一个经典翻车现场

public class User {
    private Long id;
    private String name;

    // 只重写了 equals,没有重写 hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }
}

// 使用场景
Map<User, String> map = new HashMap<>();
User u1 = new User(1L, "张三");
map.put(u1, "张三的数据");

User u2 = new User(1L, "张三");
System.out.println(map.get(u2)); // 输出 null!

分析:u1.equals(u2)true,但两者的 hashCode 不同,导致 putget 时计算出的桶索引不同,get 在错误的桶里搜寻,找不到匹配的 entry。


⚙️ 4. 哈希表内部流程(为什么必须成对重写)

一次 HashMap.put(key, value) 的操作大致分四步:

  1. 计算哈希:调用 key.hashCode(),并通过扰动函数混合高位信息,得到最终哈希值。

  2. 定位桶:根据哈希值取模,找到应插入的桶索引。

  3. 冲突处理:若该桶已有元素,遍历链表/红黑树,对每个节点调用 key.equals(k) 判断是否相等。

  4. 替换或新增:若存在 equals 相等的节点,覆盖旧值;否则插入新节点。

关键点:第 2 步只用 hashCode,第 3 步才用 equals。如果两个逻辑相等的对象 hashCode 不同,它们会散入不同的桶,第 3 步永远不会执行,导致 containsKeygetremove 全部失效。


🧠 5. 延伸:equals 不相等的对象可以共享 hashCode 吗?

完全可以,这叫哈希碰撞。比如 "Aa""BB"hashCode 都是 2112。哈希表通过链地址法或红黑树处理碰撞,先用 hashCode 缩小范围,再用 equals 精准区分。碰撞只影响性能(桶内链变长),不影响正确性。 所以 hashCode 的设计追求“均匀分布”,以降低碰撞概率。


📊 6. 成对重写的正确姿势

当你重写 equals 时,务必同时重写 hashCode,且使用相同的关键字段生成哈希码。例如:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    User user = (User) o;
    return Objects.equals(id, user.id);
}

@Override
public int hashCode() {
    return Objects.hash(id); // 用 id 生成,和 equals 保持一致
}

最佳实践:

  • 使用 IDE 自动生成(IntelliJ / Eclipse)或 Lombok 的 @EqualsAndHashCode 注解,避免手工出错。

  • 不要将可变字段作为哈希码的组成部分(除非你能保证对象存入集合后字段不会再被修改),否则会丢失对象在集合中的位置。