杰瑞科技汇

Java hashCode值到底是什么?如何高效计算?

hashCode() 是什么?

hashCode() 是 Java 中 Object 类的一个方法,这意味着所有 Java 对象都继承了这个方法

Java hashCode值到底是什么?如何高效计算?-图1
(图片来源网络,侵删)

它的核心作用是:返回一个整数值,这个整数值被称为“哈希码”(Hash Code)或“散列码”。

可以把它想象成对象的“数字指纹”或“身份证号码”,它本身并不唯一,但目的是为了在高效的数据结构(特别是 HashMap, HashSet, Hashtable)中快速地定位和存储对象。


为什么需要 hashCode()?(核心作用)

hashCode() 的主要应用场景是哈希表HashMap,为了理解它的工作原理,我们来看一下 HashMap 是如何存储数据的:

  1. 计算哈希码:当你向 HashMap 中添加一个键值对(map.put(key, value))时,HashMap 首先会调用 key 对象的 hashCode() 方法,得到一个哈希码。
  2. 计算桶索引HashMap 内部有一个数组,称为“桶”(Bucket),它会通过一个特定的算法(通常是 hash & (n-1)n 是桶的数量)将这个哈希码转换成一个数组的索引(位置)。
  3. 存储对象HashMap 就会将这个键值对存放到计算出的索引位置的桶中。

这个过程的好处是: 通过 hashCode()HashMap 可以直接计算出对象应该存储在哪个位置,而无需遍历整个数组,这使得查找、插入和删除操作的平均时间复杂度接近 O(1),效率极高,如果不用 hashCode(),每次查找都需要遍历所有元素,时间复杂度就是 O(n),效率会大大降低。

Java hashCode值到底是什么?如何高效计算?-图2
(图片来源网络,侵删)

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

这是理解 hashCode() 最关键的一点,Java 官方文档中定义了 hashCode()equals() 之间必须遵守的“契约”:

  1. 一致性:在对象的 equals() 方法没有被修改的情况下,多次调用同一个对象的 hashCode() 方法,必须返回相同的整数

    • 注意:这个要求不适用于应用程序的一次执行到另一次执行期间,也不适用于不同 JVM 的执行,你可以在一次运行中得到 123,在重启程序后得到 456,但在一次程序运行期间,只要对象内容没变,hashCode() 必须稳定。
  2. 核心规则如果两个对象根据 equals() 方法是相等的,那么这两个对象的 hashCode() 方法必须返回相同的整数。

    • 换句话说:a.equals(b)true,则 a.hashCode() 必须等于 b.hashCode()
  3. 非强制反向规则:如果两个对象的 hashCode() 值相等,它们不一定是相等的(即 a.equals(b) 不一定为 true),这被称为哈希冲突

    Java hashCode值到底是什么?如何高效计算?-图3
    (图片来源网络,侵删)
    • 两个不同的对象可能计算出相同的哈希码,就像两个不同的人可能有相同的身份证号码一样,当发生哈希冲突时,HashMap 会通过链表或红黑树在同一个桶里存储这些对象,然后通过 equals() 方法来精确比较它们是否真的相等。

总结这个契约:

比较条件 equals() 结果 hashCode() 结果 是否必须成立
两个对象相等 true 必须相等 是 (核心规则)
两个对象不相等 false 可以相等或不相等 是 (允许冲突)
hashCode() 相等 可以相等或不相等 - 是 (允许冲突)

违反契约的后果:

如果你在自定义类中只重写了 equals() 而没有重写 hashCode(),或者两者逻辑不一致,那么当你把这个类的对象作为 HashMapkey 时,会出现严重问题:

场景示例:

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    // 重写了 equals,逻辑是 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 Objects.equals(name, person.name);
    }
    // 故意不重写 hashCode()
    // @Override
    // public int hashCode() { ... }
}
public class Main {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        Person p1 = new Person("Alice");
        Person p2 = new Person("Alice");
        // p1.equals(p2) 应该返回 true
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
        map.put(p1, "Alice's Info");
        // 因为 p1.equals(p2) 是 true,我们期望 get(p2) 能找到值
        // 由于 p1 和 p2 的 hashCode() 不同(默认是对象的内存地址)
        // HashMap 会在不同的桶里查找,导致 get(p2) 返回 null
        System.out.println("map.get(p2): " + map.get(p2)); // 输出 null!
    }
}

在上面的例子中,p1p2 在逻辑上是相等的,但它们的默认 hashCode()(基于内存地址)不同。HashMap 认为它们是两个完全不同的 key,因此导致了错误的结果。


如何正确地重写 hashCode()

当你在自定义类中重写了 equals() 方法后,必须也重写 hashCode() 方法。

推荐做法:

  1. 选择一个非零的常量作为哈希码的初始值,result = 17,17 是一个质数,有助于减少哈希冲突。
  2. 为对象中参与 equals() 比较的每一个关键属性(通常是 finalprivate 的字段)计算哈希码。
  3. 对于每个属性,使用其 hashCode() 方法(如果是对象)或直接使用其值(如果是基本类型),然后将结果与 result 进行组合。
  4. 组合公式result = 31 * result + (attribute == null ? 0 : attribute.hashCode());
    • 为什么用 31?31 是一个质数,用 31 * result 可以有效地将不同的哈希码分散开。31 * i 也可以被 JVM 优化为 (i << 5) - i,性能很好。

示例:

import java.util.Objects;
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 1. 重写 equals()
    @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);
    }
    // 2. 重写 hashCode()
    @Override
    public int hashCode() {
        // 使用 Objects.hash() 是最简单、最推荐的方式!
        // 它内部就实现了上述的算法。
        return Objects.hash(name, age);
        /* 手动实现(理解原理):
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + age;
        return result;
        */
    }
}

强烈推荐使用 Objects.hash() 方法! 从 Java 7 开始,java.util.Objects 类提供了 hash(Object... values) 静态方法,它会自动帮你完成上述所有繁琐的计算,并且能很好地处理 null 值,这是现代 Java 编程的最佳实践。


hashCode() 的默认实现

如果你没有在自定义类中重写 hashCode(),那么它将使用 Object 类中的默认实现。

默认的 hashCode() 实现是基于对象的内存地址来生成一个整数值,这意味着,只有当两个对象是同一个对象(a == btrue)时,它们的默认哈希码才相等,对于任何通过 new 关键字创建的新对象,即使它们内容完全相同,它们的哈希码也不同。


特性 描述
定义 Object 类的方法,返回一个整数值(哈希码)。
主要用途 为哈希表(如 HashMap, HashSet)提供快速查找和存储的依据。
核心契约 a.equals(b)true,则 a.hashCode() 必须等于 b.hashCode()
重写规则 只要重写了 equals(),就必须重写 hashCode(),且逻辑要一致。
最佳实践 使用 Objects.hash(field1, field2, ...) 来轻松、正确地实现 hashCode()
默认行为 基于对象的内存地址,不同对象的哈希码不同。
哈希冲突 两个不同的对象可能有相同的哈希码,这是允许的,HashMap 会通过 equals() 进一步处理。

理解并正确使用 hashCode() 是成为一名合格 Java 开发者的必备技能,尤其是在处理集合框架和性能优化方面。

分享:
扫描分享到社交APP
上一篇
下一篇