为什么需要重写 hashCode()?
要回答这个问题,我们必须先理解 hashCode() 是什么,以及它和另一个关键方法 equals() 的关系。

hashCode() 的作用
hashCode() 方法返回一个整数,这个整数被称为“哈希码”或“散列码”,它的主要作用是:
- 支持哈希表数据结构:Java 中最重要的哈希表数据结构就是
HashMap、HashSet和Hashtable,这些数据结构通过哈希码来决定对象应该存储在“桶”(Bucket)中的哪个位置,从而实现数据的快速存取(平均时间复杂度为 O(1))。 - 快速判断对象“可能”相等:当两个对象的哈希码不相等时,我们可以100%确定这两个对象不相等,但如果哈希码相等,这两个对象不一定相等(这被称为“哈希冲突”或“哈希碰撞”)。
equals() 和 hashCode() 的“契约”
这是最核心的部分,Java 官方文档中规定,任何重写了 equals() 方法的类,都必须同时重写 hashCode() 方法,它们之间必须遵守以下三条“契约”:
- 一致性:如果两个对象根据
equals()方法是相等的,那么调用这两个对象的hashCode()方法必须产生相同的整数结果。a.equals(b)为true,则a.hashCode() == b.hashCode()必须为true。
- 对称性:如果两个对象根据
equals()方法是不相等的,那么调用这两个对象的hashCode()方法不要求产生不同的整数结果,为了提高哈希表的性能,强烈推荐为不相等的对象生成不同的哈希码。a.equals(b)为false,则a.hashCode() != b.hashCode()最好为true。- 如果不相等的对象产生了相同的哈希码,就会导致哈希冲突,降低哈希表的效率。
- 稳定性:在一个应用程序的多次执行过程中,同一个对象的
hashCode()方法必须返回同一个整数,前提是对象用于equals()比较的信息没有被修改。注意:在同一个执行过程中,程序可以自由地为对象改变哈希码,这通常发生在对象信息被修改后。
违反契约的后果
如果你只重写了 equals() 而没有重写 hashCode(),当你把这个对象作为 HashMap 的 Key 时,会出现严重问题:
public class Key {
private final int id;
public Key(int id) {
this.id = id;
}
// 只重写了 equals()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
return id == key.id;
}
// 没有重写 hashCode(),使用 Object 的默认实现
// @Override
// public int hashCode() { ... }
}
public class Main {
public static void main(String[] args) {
Key key1 = new Key(1);
Key key2 = new Key(1);
System.out.println(key1.equals(key2)); // 输出 true,因为它们的 id 相同
Map<Key, String> map = new HashMap<>();
map.put(key1, "Value for key1");
// 这里会出问题!
// key1 和 key2 是相等的,理论上应该能通过 key2 找到 "Value for key1"
// 因为 key1 和 key2 的 hashCode() 不同(来自 Object 类,基于内存地址)
// HashMap 会在不同的桶里查找,导致找不到值
System.out.println(map.get(key2)); // 输出 null,而不是期望的 "Value for key1"
}
}
HashMap 通过先比较 hashCode() 来快速定位可能的“桶”,然后在桶内部使用 equals() 来精确比较。hashCode() 不正确,equals() 就根本没有机会被调用,导致 HashMap 失效。

如何正确地重写 hashCode()?
重写 hashCode() 的目标是:尽可能为不相等的对象生成不同的哈希码,同时保证相等的对象哈希码相同。
Java 提供了两种主流的重写方式。
手动计算(不推荐,但有助于理解)
核心思想是将对象中所有参与 equals() 比较的字段的哈希码进行组合。
- 定义一个非零的初始值(
17)。 - 对于每一个
equals()中用到的字段,执行以下操作:result = 31 * result + (field == null ? 0 : field.hashCode())
- 返回
result。
为什么用 31?
- 31 是一个奇质数,质数能减少哈希冲突的概率。
31 * i可以被优化为(i << 5) - i,即i * 32 - i,这种位运算在现代 JVM 上比乘法运算更快。
示例:
假设我们有一个 Person 类,equals() 基于 id 和 name。
public class Person {
private int id;
private String name;
// 构造函数、getters 省略...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 手动实现
int result = 17; // 非零初始值
result = 31 * result + Integer.hashCode(id); // 使用包装类的 hashCode
result = 31 * result + (name == null ? 0 : name.hashCode());
return result;
}
}
使用 IDE 或 Objects 工具类(强烈推荐)
手动计算容易出错,而且很繁琐,现代 IDE(如 IntelliJ IDEA, Eclipse)都提供了自动生成 equals() 和 hashCode() 的功能,强烈建议使用这种方式。
IDEA 操作:
- 在类中右键 ->
Generate(或Alt + Insert)。 - 选择
equals()andhashCode()。 - 勾选所有参与比较的字段,点击
OK。
IDEA 会生成像下面这样健壮的代码:
import java.util.Objects;
public class Person {
private int id;
private String name;
// ... 其他代码 ...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// IDE 自动生成,使用了 Objects 类
return Objects.hash(id, name);
}
}
Objects.hash() 的原理
Objects.hash() 方法内部也是使用了我们上面提到的手动计算逻辑:它接收可变参数,将每个参数的哈希码与一个初始值(通常是 31)相乘后累加,它为我们做了所有繁琐的工作,代码更简洁、可读性更高。
重写 hashCode() 的最佳实践
- 总是和
equals()成对出现:如果你修改了equals(),确保同时修改hashCode(),反之亦然。 - 使用 IDE 的自动生成功能:这是最可靠、最高效的方式,可以避免人为错误。
- 只使用
equals()中用到的字段:hashCode()的计算字段必须与equals()的比较字段保持一致,否则会破坏第一条契约。 - 处理
null字段:如果字段可能为null,调用field.hashCode()会抛出NullPointerException,可以使用field == null ? 0 : field.hashCode()或者Objects.hash()会自动处理null。 - 选择好的哈希算法:目标是让哈希码尽可能均匀地分布,以减少哈希冲突,简单的
31乘法对大多数场景已经足够,如果对象有多个字段,特别是数组或集合,可以考虑更复杂的算法,如java.util.Arrays.hashCode()或java.util.Collections.hashCode()。 - 保持高性能:
hashCode()方法应该非常快,避免在其中进行复杂的计算或 I/O 操作。
| 特性 | 描述 |
|---|---|
| 核心目的 | 为了与 equals() 方法保持一致,确保 HashMap 等哈希表结构能正确工作。 |
| 核心契约 | 相等的对象必须有相同的哈希码。 |
| 常见错误 | 只重写 equals() 而忘记重写 hashCode()。 |
| 推荐实践 | 使用 IDE(如 IntelliJ IDEA)自动生成 equals() 和 hashCode() 方法。 |
| 工具类 | Objects.hash() 是生成哈希码的便捷工具。 |
| 关键字 | equals(), hashCode(), HashMap, HashSet, 契约, 哈希冲突。 |
掌握 hashCode() 的重写是成为一名合格 Java 开发者的必经之路,记住它与 equals() 的紧密关系,并善用 IDE 工具,你就能轻松应对这个问题。
