杰瑞科技汇

Java对象的hashCode有何用?

hashCode() 是什么?

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

Java对象的hashCode有何用?-图1
(图片来源网络,侵删)

返回一个整数值,这个整数值被称为该对象的“哈希码”(Hash Code)或“散列码”。

你可以把哈希码想象成对象的“数字指纹”或“身份证号”,它通常用于基于哈希算法的集合中,HashMapHashSetHashtable,这些集合被称为哈希表。


hashCode() 的核心作用:在哈希集合中定位对象

hashCode() 方法本身并没有太大的意义,它的真正威力体现在与 equals() 方法的配合使用上,尤其是在哈希集合中。

让我们以 HashMap 为例,看看它如何工作:

Java对象的hashCode有何用?-图2
(图片来源网络,侵删)
  1. 存储(put 操作)

    • 当你调用 map.put(key, value) 时,HashMap 首先会调用 key 对象的 hashCode() 方法,得到一个哈希码。
    • 这个哈希码会被哈希函数计算,确定一个“桶”(Bucket,也就是数组的索引位置)。
    • HashMap 会检查这个桶里是否已经有元素了。
      • 如果桶是空的:直接将新的键值对存入这个桶。
      • 如果桶不为空(这被称为“哈希冲突”或“碰撞”):HashMap 会遍历这个桶里的所有元素,并调用它们的 keyequals() 方法,与新的 key 进行比较。
        • equals() 返回 true,说明是同一个 key,则更新对应的 value
        • equals() 遍历完所有元素都返回 false,说明是不同的 key,则将新的键值对以链表或红黑树的形式添加到这个桶的末尾。
  2. 查询(get 操作)

    • 当你调用 map.get(key) 时,HashMap 会再次调用传入的 keyhashCode() 方法,得到哈希码。
    • 用这个哈希码计算出桶的位置。
    • 直接跳到那个桶,然后遍历桶里的元素,用 equals() 方法查找完全匹配的 key
    • 如果找到了,就返回对应的 value;如果没找到,就返回 null

关键点hashCode() 的作用是快速缩小查找范围,避免了在所有元素中进行线性查找(O(n) 的时间复杂度),而是直接定位到可能存在的那个小范围(“桶”),使得查找的平均时间复杂度接近 O(1)


hashCode()equals() 的“契约”(The Golden Rule)

这是 Java 编程中最重要的规则之一,两者必须协同工作:

Java对象的hashCode有何用?-图3
(图片来源网络,侵删)

如果两个对象根据 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"

原因分析

  1. map.put(key1, ...) 时,key1.hashCode() 被计算,假设它被放入了桶 A。
  2. map.get(key2) 时,key2.hashCode() 被计算。
    • 如果我们没有重写 hashCode()key2 会继承 ObjecthashCode(),这个方法是基于对象的内存地址计算的。key1key2 是两个不同的对象,内存地址不同,所以它们的哈希码很可能不同。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 手动实现(理解原理)

如果你想手动实现,可以遵循以下“经典公式”:

  1. 定义一个非零的常量作为哈希码的初始值,int result = 17;
  2. 对于对象中每个参与 equals() 比较的关键字段 f
    • result 乘以一个非零的质数(31):result = 31 * result;
    • 将该字段的哈希码与 result 相加:result += Objects.hashCode(f);Objects.hashCode() 能安全处理 null
  3. 返回 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,这种位运算非常快。

最佳实践和注意事项

  1. 一致性:只要对象中用于 equals() 比较的字段没有被修改,那么它的 hashCode() 必须始终返回同一个整数值,如果一个对象被存入 HashSet 后,它的 hashCode() 发生了变化,它将再也找不到了。
  2. 性能hashCode() 的计算应该尽可能快,避免在其中进行复杂的计算或 IO 操作。
  3. 不要过度优化:目标是“足够好”的哈希分布,而不是“完美”的。Objects.hash() 已经提供了非常好的通用实现。
  4. 不可变对象:将用作哈希表键(key)的对象设计为不可变(Immutable)是一个非常好的实践,因为一旦对象被放入集合,它的 hashCode 就不能改变,这保证了集合的稳定性和正确性。
特性 描述
定义 hashCode() 返回一个对象的整型哈希码,用于快速定位和比较。
核心作用 HashMapHashSet 等哈希集合中,通过哈希码快速确定对象的存储位置(桶),实现高效的查找和插入。
equals() 的契约 必须遵守a.equals(b)truea.hashCode() 必须等于 b.hashCode()
违反契约的后果 哈希集合将无法正常工作,get()remove() 等操作可能失效,导致找不到已存在的元素。
默认实现 基于内存地址生成,不同对象的哈希码几乎总是不同。
重写时机 只要你重写了 equals() 方法,就必须重写 hashCode() 方法。
重写方法 推荐使用 Objects.hash(field1, field2, ...)
理解原理可以使用 31 * result + Objects.hashCode(field) 的经典公式。
最佳实践 将用作 key 的对象设计为不可变,以保证哈码的稳定性。
分享:
扫描分享到社交APP
上一篇
下一篇