前言
在 Java 中,hashCode()
和 equals()
是 Object 类的两个核心方法,二者紧密配合,尤其在哈希集合(如 HashMap、HashSet)中发挥关键作用。理解它们的关系,是写出高效、无 Bug 代码的基础。
一、先明确:两者的"本职工作"
在讲关系前,需先清楚各自的作用------它们的设计初衷就决定了必须协同工作。

二、核心关系:三大"黄金法则"
这是 Java 官方定义的强制约定,违反则会导致哈希集合(如 HashMap)出现逻辑错误:
-
法则1:若
a.equals(b) == true
,则a.hashCode() == b.hashCode()
必须成立哈希集合的工作逻辑是"先找桶,再比对象":先通过
hashCode()
定位对象所在的哈希桶,再在桶内用equals()
逐个对比。若相等的对象哈希值不同,会被分到不同桶中,导致哈希集合认为"这是两个不同对象",出现存不进、查不到的 Bug。 -
法则2:若
a.hashCode() == b.hashCode()
,a.equals(b)
不一定为 true哈希值是 int 类型(范围 -2³¹ ~ 2³¹-1),而对象的数量远超过这个范围,必然会出现"哈希碰撞"(不同对象哈希值相同)。此时需要通过
equals()
进一步判断对象是否真的相等。 -
法则3:若
a.equals(b) == false
,a.hashCode()
和b.hashCode()
可同可不同不强制要求不等的对象哈希值不同,但尽量让它们不同------若大量不等的对象哈希值相同,会导致哈希桶内元素过多,查询效率从 O(1) 退化到 O(n)。
三、反例:违反约定会导致什么问题?
假设我们自定义一个 User
类,只重写 equals()
但不重写 hashCode()
,看看在 HashMap 中会出现什么问题:
java
class User {
private Integer id;
private String name;
// 构造器、getter、setter 省略
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
// 只重写 equals():id 相同则认为相等
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
// 未重写 hashCode(),使用 Object 类的默认实现
// Object.hashCode() 返回对象的内存地址相关值,不同对象哈希值不同
}
// 测试代码
public class HashCodeEqualsTest {
public static void main(String[] args) {
User u1 = new User(1, "张三");
User u2 = new User(1, "张三");
// 1. equals() 结果:true(因为 id 相同)
System.out.println("u1.equals(u2) = " + u1.equals(u2)); // 输出 true
// 2. hashCode() 结果:不同(默认实现依赖内存地址)
System.out.println("u1.hashCode() = " + u1.hashCode()); // 例如:1163157884
System.out.println("u2.hashCode() = " + u2.hashCode()); // 例如:1956725890
// 3. 放入 HashMap,出现 Bug!
Map<User, String> userMap = new HashMap<>();
userMap.put(u1, "张三的信息");
// 期望:能通过 u2 查到值,但实际返回 null
System.out.println("userMap.get(u2) = " + userMap.get(u2)); // 输出 null
}
}
问题原因 :
u1
和 u2
的 equals()
为 true,但 hashCode()
不同。HashMap 存储 u1
时,通过 u1.hashCode()
定位到桶 A;查询 u2
时,通过 u2.hashCode()
定位到桶 B------两个不同的桶,自然查不到 u1
的值,违背了"逻辑相等的对象应被视为同一个键"的预期。
四、正确实践:重写 equals() 必须同时重写 hashCode()
遵循"相等的对象必须有相等的哈希值",重写时需保证:equals() 中用到的字段,必须全部参与 hashCode() 的计算。
以 User
类为例,正确重写如下:
java
import java.util.Objects;
import java.util.HashMap;
import java.util.Map;
class User {
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
// 重写 equals():id 和 name 都相同,才认为相等
@Override
public boolean equals(Object o) {
if (this == o) return true; // 先判断内存地址(快速短路)
if (o == null || getClass() != o.getClass()) return false; // 非空、同类型校验
User user = (User) o;
// equals() 依赖的字段:id 和 name
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
// 重写 hashCode():必须包含 equals() 中用到的所有字段(id 和 name)
@Override
public int hashCode() {
// Objects.hash() 会自动处理 null,避免空指针
return Objects.hash(id, name);
}
// getter 省略(测试用)
public Integer getId() { return id; }
public String getName() { return name; }
}
// 测试:正确工作
public class CorrectTest {
public static void main(String[] args) {
User u1 = new User(1, "张三");
User u2 = new User(1, "张三");
// equals() 为 true,hashCode() 也相等
System.out.println("u1.equals(u2) = " + u1.equals(u2)); // true
System.out.println("u1.hashCode() = " + u1.hashCode()); // 例如:68532510
System.out.println("u2.hashCode() = " + u2.hashCode()); // 例如:68532510
// HashMap 能正确查询
Map<User, String> userMap = new HashMap<>();
userMap.put(u1, "张三的信息");
System.out.println("userMap.get(u2) = " + userMap.get(u2)); // 输出:张三的信息
}
}
关键技巧 :
使用 Objects.hash(字段1, 字段2, ...)
重写 hashCode()
,既简洁又能避免 null 指针(若字段为 null,Objects.hash()
会将其哈希值视为 0)。
五、总结
- 核心关系 :
equals()
是"逻辑相等"的最终判断,hashCode()
是"快速定位"的辅助;相等的对象必须有相等的哈希值,哈希值相等的对象不一定相等。 - 必守原则 :重写
equals()
时,必须同步重写hashCode()
,且两者依赖的字段必须完全一致。 - 应用场景 :在使用 HashMap、HashSet、HashTable 等哈希集合时,若自定义对象作为键(或元素),务必保证
equals()
和hashCode()
正确重写,否则会出现逻辑错误。
记住:两者的设计是"分工协作",而非"各自独立"------理解这一点,才能避免 Java 开发中最常见的哈希集合 Bug。