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,说明它们逻辑上是同一个用户。但是,用 user2 去 map.get() 却返回了 null,仿佛这个键从未被存入过。
2. 诊断原因:HashMap 的工作原理
要理解这个异常,必须知道 HashMap 内部是如何存储和查找键值对的。其过程分为两步:
-
第一步:定位“桶”
- 当你调用
map.put(key, value)或map.get(key)时,HashMap首先会调用键对象的hashCode()方法,计算出一个哈希码。 - 然后,通过一个扰动函数,将这个哈希码转换成一个数组索引(即确定存入或查找哪个“桶”)。
- 当你调用
-
第二步:桶内比较
- 定位到桶后,
HashMap会遍历这个桶内(可能存在哈希冲突)的所有键值对。 - 它使用
equals()方法来逐一比较,找到与传入键equals为true的条目。
- 定位到桶后,
问题的根源就在于第一步。虽然我们重写了 equals(),使 user1 和 user2 在第二步比较时能通过,但我们没有重写 hashCode()。因此,user1 和 user2 继承自 Object 类的默认 hashCode() 方法,该方法通常根据对象的内存地址生成一个唯一的整数。
user1和user2是new出来的两个不同对象,内存地址不同,所以它们的hashCode()值大概率不同。- 结果就是:
user1被存入了hashCode值对应的桶A,而user2去get时,计算出了另一个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);
}
}
关键动作:
- 重写
hashCode()方法。 - 使用
Objects.hash()工具方法,它接收的参数应与equals()方法中用于比较的字段完全一致。 - 运行之前的测试代码,现在
map.get(user2)将会正确返回"第一条数据"。
5. 最佳实践与自检清单
- IDE 自动生成:在 IntelliJ IDEA 或 Eclipse 等IDE中,你可以将光标放在类体内,使用快捷键(如
Alt+Insert或Command+N)选择 “Generate equals() and hashCode()”,IDE会帮你基于选定的字段生成标准、正确的实现。 - 一致性字段:确保
hashCode()和equals()使用的字段集完全相同。如果equals比较了id和name,那么hashCode也必须由id和name共同计算。 - 不可变对象:作为
HashMap键的对象,其参与哈希计算的字段最好是不可变的(final),否则对象存入后哈希值改变,会导致后续查找永久失效。 - Lombok 注解:如果项目使用 Lombok,在类上直接添加
@EqualsAndHashCode注解即可自动生成基于所有非静态字段的标准实现。
自检你的代码:当你发现一个自定义对象作为 HashMap 键出现查询异常时,第一步就是检查这个类是否同时正确地重写了 equals() 和 hashCode() 方法。

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