这是一个在 Java 面试和日常开发中都非常重要的知识点,因为重写 hashCode() 必须与重写 equals() 方法一起进行,否则会导致严重的问题。

为什么需要重写 hashCode()?
我们要明白 hashCode() 方法是做什么的,它的主要作用有两个:
- 哈希表(如
HashMap,HashSet,Hashtable)的“路由”:当向HashMap这样的集合中添加一个对象时,HashMap会先调用这个对象的hashCode()方法,得到一个哈希码(一个整数)。HashMap内部会通过这个哈希码计算出对象应该被存放在哪个“桶”(Bucket)里,这使得查找和插入操作的平均时间复杂度接近 O(1),极大地提高了效率。 - 作为对象的“数字指纹”:
hashCode()返回一个整数值,可以看作是这个对象在内存中的“指纹”,理想情况下,两个不同的对象应该有不同的“指纹”。
hashCode() 和 equals() 的“契约”
这是最核心、最必须遵守的规则,Java 官方文档(Object 类的 hashCode() 方法的规范)定义了以下契约:
只要
equals()方法比较的两个对象是“相等”的,那么这两个对象的hashCode()方法必须返回相同的整数值。注意:反过来说不成立,如果两个对象的
hashCode()值相同,它们不一定相等(这被称为“哈希碰撞”),如果两个对象的hashCode()值不同,那么它们一定不相等。(图片来源网络,侵删)
这个契约意味着:
- 对象相等,哈希码必须相等:
a.equals(b)返回true,a.hashCode()必须等于b.hashCode(),这是强制要求,不能违反。 - 哈希码相等,对象不一定相等:
a.hashCode()等于b.hashCode(),a.equals(b)可能返回true,也可能返回false。
违反契约的后果
如果你只重写了 equals() 而没有重写 hashCode(),或者反之,会发生什么?
场景:将一个自定义对象 Person 放入 HashSet 中。

// 假设 Person 类只重写了 equals(),没有重写 hashCode()
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
// p1.equals(p2) 应该返回 true
System.out.println(p1.equals(p2)); // true
HashSet<Person> set = new HashSet<>();
set.add(p1);
System.out.println("Set size after adding p1: " + set.size()); // 1
// 因为 p1.equals(p2) 是 true,我们期望 p2 不会被添加,set 大小仍为 1
set.add(p2);
System.out.println("Set size after adding p2: " + set.size()); // !!! 问题来了,这里可能会是 2 !!!
为什么会这样?
- 当
set.add(p1)时,HashSet会计算p1.hashCode()得到值H1,然后根据H1将p1放入某个桶中。 - 当
set.add(p2)时,HashSet会计算p2.hashCode(),由于Person没有重写hashCode(),它调用的是Object类的hashCode()方法,该方法基于对象的内存地址。p1和p2是两个不同的对象实例,所以它们的内存地址不同,p1.hashCode()和p2.hashCode()的值也不同。 HashSet发现哈希码不同,就认为p1和p2是两个完全不同的对象,于是将p2放入了另一个桶中。set中就包含了两个我们认为“相等”的对象,这显然是错误的行为。
为了保证基于哈希的集合(HashMap, HashSet 等)的正确性,一旦你重写了 equals(),就必须重写 hashCode()。
如何正确地重写 hashCode()?
重写 hashCode() 的目标是:尽可能让 equals() 返回 true 的对象拥有相同的 hashCode,同时让不同的对象拥有不同的 hashCode,以减少哈希碰撞。
手动实现(不推荐,但有助于理解)
你需要选择一个算法,将对象的“关键属性”组合成一个整数,一个经典且有效的算法是:
- 声明一个非零的常量作为哈希码的初始值(
17)。 - 为每个参与
equals()比较的关键属性计算一个“哈希码因子”。 - 将初始值与每个属性的哈希码因子进行“异或”运算,并乘以一个质数(
31)。 - 返回最终结果。
为什么用 31?
31是一个质数,能减少哈希碰撞的概率。31 * i可以被优化为(i << 5) - i,即i * 32 - i,这种位运算在 JVM 上非常快。
示例:重写 Person 类的 hashCode()
假设 Person 类有两个关键属性:name (String) 和 age (int)。
public class Person {
private String name;
private int age;
// 构造函数、getters、setters 省略...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 1. 声明一个非零常量
int result = 17;
// 2. 为每个关键属性计算哈希码因子并组合
// 31 是一个常用的质数
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + age;
return result;
}
}
使用 IDE 自动生成(推荐)
在实际开发中,手动编写容易出错,IDE 非常擅长生成符合规范的代码,这是最常用、最可靠的方法。
以 IntelliJ IDEA 为例:
- 在类中右键 ->
Generate(或按Alt + Insert)。 - 选择
equals() and hashCode()。 - IDE 会弹出一个窗口,让你选择要包含在
equals()和hashCode()计算中的字段,确保你选择的字段与equals()方法中使用的字段一致。 - 点击
OK,IDE 就会自动生成高质量的代码。
IDE 生成的代码通常和手动实现的方法类似,但更简洁,使用了 java.util.Objects 工具类。
IDE 生成的代码示例:
import java.util.Objects;
public class Person {
private String name;
private int age;
// ... 其他代码 ...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Objects.hash() 的优点:
- 简洁:一行代码搞定。
- 安全:自动处理了
null值,无需像手动实现那样写field == null ? 0 : field.hashCode()。 - 高效:内部也使用了类似的质数乘法算法来组合哈希码。
注意:虽然 Objects.hash() 非常方便,但如果你对性能有极致要求,并且你的对象是“热对象”(被频繁创建和使用),手动实现可能会因为避免了数组的创建而有一点点性能优势,但在绝大多数场景下,Objects.hash() 的便利性远胜于那点微不足道的性能差异。
使用 Lombok(如果项目允许)
如果你的项目中使用了 Lombok 库,事情就变得更简单了。
只需在类上添加 @EqualsAndHashCode 注解即可。
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class Person {
private String name;
private int age;
// ... 其他代码 ...
}
Lombok 会在编译时自动为你生成 equals() 和 hashCode() 方法,其逻辑和 IDE 生成的类似,这是目前最省事的方式。
重写 hashCode() 的最佳实践
- 始终与
equals()一起重写:这是铁律。 - 使用关键属性:参与
hashCode()计算的字段列表,必须与参与equals()计算的字段列表完全一致。 - 优先使用 IDE 或 Lombok:不要手动编写,除非有特殊需求或出于学习目的。
- 考虑不可变性:如果一个对象是不可变的(创建后其属性不能被修改),那么它的
hashCode只需计算一次并缓存起来,这能提高性能,在final字段中缓存哈希码。 - 不要过度优化:不要为了追求完美的哈希分布而引入复杂的计算,一个好的哈希函数的目标是“足够好”,而不是“完美”,简单的质数乘法算法已经足够高效。
| 问题 | 答案 |
|---|---|
| 为什么要重写? | 为了让 equals() 相等的对象在 HashMap, HashSet 等哈希集合中被视为同一个对象,保证集合逻辑的正确性。 |
与 equals() 的关系? |
契约:equals() 为 true 的对象,hashCode() 必须相等。 |
| 如何重写? | 推荐:使用 IDE 的 Generate equals() and hashCode() 功能,或使用 Lombok 的 @EqualsAndHashCode 注解。 |
| 核心原则? | hashCode() 的计算字段必须与 equals() 的计算字段完全一致。 |
掌握了这些,你就能在 Java 开发中正确、自信地处理 hashCode() 的重写问题了。

