hashCode() 是什么?
hashCode() 是 Java 中所有对象都拥有的一个方法,它定义在 Object 类中,它的作用是:

返回一个整数值,这个整数值被称为该对象的“哈希码”(Hash Code)或“散列码”。
你可以把哈希码想象成对象的“数字指纹”或“身份证号”,它通常用于基于哈希算法的集合中,HashMap、HashSet 和 Hashtable,这些集合被称为哈希表。
hashCode() 的核心作用:在哈希集合中定位对象
hashCode() 方法本身并没有太大的意义,它的真正威力体现在与 equals() 方法的配合使用上,尤其是在哈希集合中。
让我们以 HashMap 为例,看看它如何工作:

-
存储(
put操作):- 当你调用
map.put(key, value)时,HashMap首先会调用key对象的hashCode()方法,得到一个哈希码。 - 这个哈希码会被哈希函数计算,确定一个“桶”(Bucket,也就是数组的索引位置)。
HashMap会检查这个桶里是否已经有元素了。- 如果桶是空的:直接将新的键值对存入这个桶。
- 如果桶不为空(这被称为“哈希冲突”或“碰撞”):
HashMap会遍历这个桶里的所有元素,并调用它们的key的equals()方法,与新的key进行比较。equals()返回true,说明是同一个key,则更新对应的value。equals()遍历完所有元素都返回false,说明是不同的key,则将新的键值对以链表或红黑树的形式添加到这个桶的末尾。
- 当你调用
-
查询(
get操作):- 当你调用
map.get(key)时,HashMap会再次调用传入的key的hashCode()方法,得到哈希码。 - 用这个哈希码计算出桶的位置。
- 直接跳到那个桶,然后遍历桶里的元素,用
equals()方法查找完全匹配的key。 - 如果找到了,就返回对应的
value;如果没找到,就返回null。
- 当你调用
关键点:hashCode() 的作用是快速缩小查找范围,避免了在所有元素中进行线性查找(O(n) 的时间复杂度),而是直接定位到可能存在的那个小范围(“桶”),使得查找的平均时间复杂度接近 O(1)。
hashCode() 和 equals() 的“契约”(The Golden Rule)
这是 Java 编程中最重要的规则之一,两者必须协同工作:

如果两个对象根据
equals()方法比较是相等的,那么这两个对象的hashCode()方法必须返回相同的整数。
反之则不成立:如果两个对象的 hashCode() 相同,它们不一定相等,这只是一个“哈希碰撞”,equals() 方法会进行最终的裁决。
为什么必须遵守这个契约?
假设我们违反了它,会发生什么?
// 假设我们有一个错误的 MyKey 类
class MyKey {
private int id;
public MyKey(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyKey myKey = (MyKey) o;
return id == myKey.id;
}
// 错误:没有重写 hashCode(),或者重写的逻辑不对
// @Override
// public int hashCode() {
// return 1; // 总是返回1,导致所有对象都哈希到同一个桶
// }
}
// --- 使用场景 ---
Map<MyKey, String> map = new HashMap<>();
MyKey key1 = new MyKey(1);
map.put(key1, "Value for key 1");
// 我们创建一个与 key1 相等的新对象
MyKey key2 = new MyKey(1);
// 根据契约,key1.equals(key2) 应该是 true
// 尝试获取 value
String value = map.get(key2);
System.out.println(value); // 输出 null,而不是 "Value for key 1"
原因分析:
map.put(key1, ...)时,key1.hashCode()被计算,假设它被放入了桶 A。map.get(key2)时,key2.hashCode()被计算。- 如果我们没有重写
hashCode(),key2会继承Object的hashCode(),这个方法是基于对象的内存地址计算的。key1和key2是两个不同的对象,内存地址不同,所以它们的哈希码很可能不同。HashMap会根据key2的哈希码去一个错误的桶里查找,自然找不到。 - 如果我们像上面注释里那样,错误地让
hashCode()总是返回1,key2的哈希码会指向桶 A。HashMap会去桶 A 里查找,然后遍历元素,调用key1.equals(key2),由于我们正确实现了equals(),它会返回true,所以在这种情况下,get(key2)能找到值。如果key1.hashCode()和key2.hashCode()因为任何原因(比如哈希函数的随机性)不同,get操作就会失败。
- 如果我们没有重写
为了保证哈希集合的正确性,只要你重写了 equals() 方法,就必须重写 hashCode() 方法,以确保“相等对象有相同哈希码”。
默认的 hashCode() 实现
如果你不重写 hashCode() 方法,那么你的类将使用 Object 类中的默认实现,这个实现是基于对象的内存地址来生成一个哈希码的。
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1.hashCode()); // 输出一个基于内存地址的整数,123456789
System.out.println(obj2.hashCode()); // 输出另一个基于内存地址的整数,987654321
// 即使两个对象内容完全相同,它们也是不同的实例,内存地址不同
MyObject a = new MyObject("hello");
MyObject b = new MyObject("hello");
System.out.println(a.equals(b)); // 取决于 MyObject 是否重写了 equals
System.out.println(a.hashCode()); // 123456789
System.out.println(b.hashCode()); // 987654321
如何正确地重写 hashCode()?
重写 hashCode() 的目标是:尽可能让“不相等”的对象产生不同的哈希码,以减少哈希冲突,提高哈希表的性能。
1 简单但有效的方法(推荐)
现代 Java 版本(Java 7+)推荐使用 Objects.hash() 工具方法,它会自动为你处理所有计算,并且能很好地处理 null 值。
import java.util.Objects;
public class Person {
private String name;
private int age;
private String ssn; // 社会安全号,假设它是唯一标识符
// ... 构造函数, getters, setters ...
@Override
public boolean equals(Object o) {
// ... equals 实现 ...
}
@Override
public int hashCode() {
// 推荐:使用 Objects.hash()
// 它会为每个参数调用 Objects.hashCode(),然后将结果组合起来
return Objects.hash(ssn); // 假设 ssn 是判断对象相等的核心
}
}
2 手动实现(理解原理)
如果你想手动实现,可以遵循以下“经典公式”:
- 定义一个非零的常量作为哈希码的初始值,
int result = 17;。 - 对于对象中每个参与
equals()比较的关键字段f:- 将
result乘以一个非零的质数(31):result = 31 * result; - 将该字段的哈希码与
result相加:result += Objects.hashCode(f);(Objects.hashCode()能安全处理null)
- 将
- 返回
result。
import java.util.Objects;
public class Person {
private String name;
private int age;
private String ssn;
// ... 其他代码 ...
@Override
public int hashCode() {
// 1. 初始化一个非零常量
int result = 17;
// 2. 处理每个关键字段
result = 31 * result + Objects.hashCode(name);
result = 31 * result + Integer.hashCode(age);
result = 31 * result + Objects.hashCode(ssn);
// 3. 返回结果
return result;
}
}
为什么用质数 31?
- 数学性质:31 是一个奇质数,用质数相乘可以减少哈希冲突的概率。
- 性能:
31 * i可以被 JVM 优化为(i << 5) - i,即(i * 2^5) - i,这种位运算非常快。
最佳实践和注意事项
- 一致性:只要对象中用于
equals()比较的字段没有被修改,那么它的hashCode()必须始终返回同一个整数值,如果一个对象被存入HashSet后,它的hashCode()发生了变化,它将再也找不到了。 - 性能:
hashCode()的计算应该尽可能快,避免在其中进行复杂的计算或 IO 操作。 - 不要过度优化:目标是“足够好”的哈希分布,而不是“完美”的。
Objects.hash()已经提供了非常好的通用实现。 - 不可变对象:将用作哈希表键(
key)的对象设计为不可变(Immutable)是一个非常好的实践,因为一旦对象被放入集合,它的hashCode就不能改变,这保证了集合的稳定性和正确性。
| 特性 | 描述 |
|---|---|
| 定义 | hashCode() 返回一个对象的整型哈希码,用于快速定位和比较。 |
| 核心作用 | 在 HashMap、HashSet 等哈希集合中,通过哈希码快速确定对象的存储位置(桶),实现高效的查找和插入。 |
与 equals() 的契约 |
必须遵守:a.equals(b) 为 true,a.hashCode() 必须等于 b.hashCode()。 |
| 违反契约的后果 | 哈希集合将无法正常工作,get()、remove() 等操作可能失效,导致找不到已存在的元素。 |
| 默认实现 | 基于内存地址生成,不同对象的哈希码几乎总是不同。 |
| 重写时机 | 只要你重写了 equals() 方法,就必须重写 hashCode() 方法。 |
| 重写方法 | 推荐使用 Objects.hash(field1, field2, ...)。理解原理可以使用 31 * result + Objects.hashCode(field) 的经典公式。 |
| 最佳实践 | 将用作 key 的对象设计为不可变,以保证哈码的稳定性。 |
