文章目录

Javaequals被重写后hashCode不同步导致HashMap查询异常

发布于 2026-06-08 21:37:24 · 浏览 15 次 · 评论 0 条

Java equals 被重写后 hashCode 不同步导致 HashMap 查询异常

当我们将一个自定义对象作为 HashMap 的键(Key)时,如果只重写了 equals() 方法,而没有同步重写 hashCode() 方法,会导致一个隐蔽且严重的问题:明明是“相同”的对象,却无法从 HashMap 中取到对应的值。本文将直接带你定位问题、理解原理并完成修复。


1. 复现问题:一个典型的错误示例

首先,我们创建一个普通的 User 类,并只重写 equals() 方法,使其根据用户ID判断对象是否相等。

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

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // 省略 Getter 和 Setter

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User user = (User) obj;
        return id.equals(user.id);
    }
    // 注意:没有重写 hashCode() 方法!
}

现在,编写测试代码,使用这个 User 对象作为 HashMap 的键。

import java.util.HashMap;
import java.util.Map;

public class HashMapTest {
    public static void main(String[] args) {
        Map<User, String> map = new HashMap<>();

        // 创建第一个用户对象 user1
        User user1 = new User("1001", "Alice");
        // 将 user1 作为键存入 map
        map.put(user1, "第一条数据");

        // 创建第二个用户对象 user2,其 ID 与 user1 相同
        User user2 = new User("1001", "Alice");

        // 检查 user1 和 user2 是否“相等”
        System.out.println("user1.equals(user2): " + user1.equals(user2));
        // 输出:true

        // 尝试用 user2 去获取 map 中的值
        String result = map.get(user2);
        System.out.println("用 user2 获取数据: " + result);
        // 输出:null
    }
}

关键现象user1.equals(user2) 的结果是 true,说明它们逻辑上是同一个用户。但是,用 user2map.get() 却返回了 null,仿佛这个键从未被存入过。


2. 诊断原因:HashMap 的工作原理

要理解这个异常,必须知道 HashMap 内部是如何存储和查找键值对的。其过程分为两步:

  1. 第一步:定位“桶”

    • 当你调用 map.put(key, value)map.get(key) 时,HashMap 首先会调用键对象的 hashCode() 方法,计算出一个哈希码
    • 然后,通过一个扰动函数,将这个哈希码转换成一个数组索引(即确定存入或查找哪个“桶”)。
  2. 第二步:桶内比较

    • 定位到桶后,HashMap 会遍历这个桶内(可能存在哈希冲突)的所有键值对。
    • 它使用 equals() 方法来逐一比较,找到与传入键 equalstrue 的条目。

问题的根源就在于第一步。虽然我们重写了 equals(),使 user1user2 在第二步比较时能通过,但我们没有重写 hashCode()。因此,user1user2 继承自 Object 类的默认 hashCode() 方法,该方法通常根据对象的内存地址生成一个唯一的整数。

  • user1user2new 出来的两个不同对象,内存地址不同,所以它们的 hashCode() 值大概率不同。
  • 结果就是user1 被存入了 hashCode 值对应的桶A,而 user2get 时,计算出了另一个 hashCode 值,被定向到了另一个桶B。
  • 在桶B中根本找不到任何与 user2 匹配的键,所以返回 null。查询流程甚至根本没机会执行到第二步的 equals() 比较。

3. 核心规则:重写 equals 必须重写 hashCode

Java 的通用契约规定:

如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象的 hashCode() 方法都必须产生相同的整数结果。

反过来,如果两个对象 hashCode 相同,它们并不一定 equals(这是哈希冲突)。但违反上述契约,就会破坏所有依赖于哈希的集合(如 HashMap, HashSet)的正常工作。


4. 修复代码:同步实现 hashCode 和 equals

回到 User 类,同步重写 hashCode() 方法。推荐使用所有参与 equals 比较的字段(这里是 id)来生成哈希码。

import java.util.Objects;

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

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // 省略 Getter 和 Setter

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User user = (User) obj;
        return id.equals(user.id);
    }

    @Override
    public int hashCode() {
        // 使用参与 equals 比较的字段(id)来计算 hashCode
        return Objects.hash(id);
    }
}

关键动作

  1. 重写 hashCode() 方法。
  2. 使用 Objects.hash() 工具方法,它接收的参数应与 equals() 方法中用于比较的字段完全一致
  3. 运行之前的测试代码,现在 map.get(user2) 将会正确返回 "第一条数据"

5. 最佳实践与自检清单

  1. IDE 自动生成:在 IntelliJ IDEA 或 Eclipse 等IDE中,你可以将光标放在类体内,使用快捷键(如 Alt+InsertCommand+N)选择 “Generate equals() and hashCode()”,IDE会帮你基于选定的字段生成标准、正确的实现。
  2. 一致性字段:确保 hashCode()equals() 使用的字段集完全相同。如果 equals 比较了 idname,那么 hashCode 也必须由 idname 共同计算。
  3. 不可变对象:作为 HashMap 键的对象,其参与哈希计算的字段最好是不可变的final),否则对象存入后哈希值改变,会导致后续查找永久失效。
  4. Lombok 注解:如果项目使用 Lombok,在类上直接添加 @EqualsAndHashCode 注解即可自动生成基于所有非静态字段的标准实现。

自检你的代码:当你发现一个自定义对象作为 HashMap 键出现查询异常时,第一步就是检查这个类是否同时正确地重写了 equals()hashCode() 方法

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文