为什么重写 equals 时一定要重写 hashCode?

#Java#面试八股

为什么重写 equals 时一定要重写 hashCode?

结论:因为 Object 的通用约定(general contract)要求——equals 判定相等的两个对象,hashCode 必须相等。这条约定是哈希容器(hash-based collection)正确性的前提:HashMap/HashSet 先用 hashCode 定位桶(bucket),再用 equals 在桶内比较。只重写 equals 会让"相等"的对象落到不同的桶里,导致 put 进去 get 不出来、Set 里出现"重复"元素。

1. 哈希容器怎么用这两个方法

map.put(key, value)                        map.get(key)
        │                                        │
        ▼                                        ▼
  hash = spread(key.hashCode())          hash = spread(key.hashCode())
        │                                        │
        ▼                                        ▼
  index = (n - 1) & hash                 index = (n - 1) & hash
        │                                        │
        ▼                                        ▼
  桶空?→ 直接放                         遍历该桶链表/红黑树
  桶非空 → 逐个比较:                    对每个节点判断:
     (k == key) || key.equals(k)           (k == key) || key.equals(k)
        │                                        │
        ▼                                        ▼
      命中 → 覆盖                        命中 → 返回 value,否则 null

关键点:hashCode 决定"去哪个桶找",equals 决定"桶里哪个是它"。两步缺一不可,如果 hashCode 不一致,equals 根本没有机会被调用。

2. 只重写 equals 会怎样

class OrderNo {
    private final String value;
    OrderNo(String value) { this.value = value; }

    @Override public boolean equals(Object o) {           // 只重写 equals
        if (this == o) return true;
        if (!(o instanceof OrderNo)) return false;
        return value.equals(((OrderNo) o).value);
    }
}

Set<OrderNo> set = new HashSet<>();
set.add(new OrderNo("A001"));
set.add(new OrderNo("A001"));
System.out.println(set.size());            // 2,重复元素"漏"进来了

Map<OrderNo, String> map = new HashMap<>();
map.put(new OrderNo("A001"), "已支付");
System.out.println(map.get(new OrderNo("A001")));  // null,取不出来

两次 new 的对象 hashCode 是 Object 给的标识哈希(identity hash),必然不同,于是进入两个不同的桶,equals 从未参与比较。

3. 四种组合的结果

重写情况equals 语义hashCode后果
都不重写引用相等标识哈希只有同一对象才相等
只重写 equals内容相等标识哈希违反契约,哈希容器失效
只重写 hashCode引用相等内容哈希哈希相同但 equals 不等,仍不等
都重写内容相等内容哈希正确
都重写但字段可变内容相等随字段变化key 的哈希改变,get 找不到(内存泄漏)

4. 正确写法

import java.util.Objects;

public final class OrderNo {
    private final String value;

    public OrderNo(String value) { this.value = value; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof OrderNo)) return false;
        OrderNo other = (OrderNo) o;
        return value.equals(other.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);       // 与 equals 使用同一组字段
    }
}

三条纪律:

  1. 字段一致:equals 用哪些字段,hashCode 就用哪些字段;
  2. 不可变:作为 key 的字段必须是 final 的,否则入表后哈希变化;
  3. 对称与传递:equals 要满足自反、对称、传递、一致、非空五个约定,不要拿 getClass() 和 instanceof 混用。

如果是 Java 16+ 的 record,编译器自动生成基于全部组件的 equals/hashCode,无需手写;Lombok 的 @EqualsAndHashCode 也做同样的事,但要留意它默认包含全部非静态字段。

5. 常见追问

追问回答要点
hashCode 相同,equals 一定相等吗?不一定,哈希冲突(collision)允许不同对象同哈希,所以桶内还要比 equals
equals 相等,hashCode 一定相等吗?必须相等,否则契约被破坏
hashCode 返回常量 0 合法吗?合法,但所有元素挤进一个桶,退化成链表 O(n)
为什么用 Objects.hash 而不是手写?简洁、可读、自动处理 null;代价是可变参数会创建数组,极致性能场景可手写
equals 用 instanceof 还是 getClass()?允许子类相等用 instanceof;要求严格同类型用 getClass(),继承体系里优先考虑 final 类
可变对象做 key 有什么后果?哈希改变后桶位置错乱,get 返回 null,且旧节点长期滞留形成逻辑泄漏
只重写 hashCode 行不行?不行,HashMap 桶内仍用 equals 比较,引用不等就作为新节点插入

小结

一句话:hashCode 负责"找得到",equals 负责"认得对"。重写时两者必须基于同一组字段、同时重写;作为 key 的对象要保持不可变。Objects.hash 是标准写法,record/Lombok 是省事写法。

笔记加载中…