杰瑞科技汇

Java hashCode()方法如何正确使用?

这是一个在 Java 编程中至关重要但又常常被误解的方法,理解它对于编写高性能、健壮的代码,尤其是在使用集合框架(如 HashMap, HashSet)时,必不可少。

Java hashCode()方法如何正确使用?-图1
(图片来源网络,侵删)

hashCode() 是什么?

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

它的官方定义是:

返回该对象的哈希码值,这个方法是为哈希表(HashMap 提供的)提供的,以便更好地工作。

通用约定

Java hashCode()方法如何正确使用?-图2
(图片来源网络,侵删)
  1. 在 Java 应用程序执行期间,只要用于 equals 比较的信息没有被修改,那么对同一个对象多次调用 hashCode() 方法,必须一致地返回同一个整数
  2. 如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象的 hashCode() 方法必须产生相同的整数结果
  3. 不要求:如果两个对象根据 equals(Object) 方法是不相等的,那么调用这两个对象的 hashCode() 方法不一定需要产生不同的整数结果,程序员应该知道,为不相等的对象生成不同的哈希码值可以提高哈希表的性能

hashCode() 返回一个整数值,这个值可以看作是对象的“数字指纹”或“身份证号”。


hashCode()equals() 的“神圣契约”

这是理解 hashCode() 最核心、最关键的一点。hashCode()equals() 之间存在着一个不可分割的约定,通常被称为 “hashCode-equals 契约” (hashCode-equals Contract)

  1. a.equals(b)truea.hashCode() 必须等于 b.hashCode()

    • 解释:如果两个对象被认为是“相等”的,那么它们必须在哈希表中存储在同一个“桶”(bucket)里,为了找到同一个桶,它们的哈希码必须相同,如果违反了这一点,当你把 a 放进 HashMap 后,再用 b 去查找时,会因为哈希码不同而直接去错误的桶里查找,导致 get(b) 返回 null,即使 ab 是相等的。
    • 后果:严重破坏集合框架的正确性。
  2. a.hashCode() 等于 b.hashCode()a.equals(b) 不一定为 true

    Java hashCode()方法如何正确使用?-图3
    (图片来源网络,侵删)
    • 解释:哈希码是一个有限的整数(32位),而对象的可能性是无限的,多个不同的对象(称为“哈希冲突”或“哈希碰撞”)可能会计算出相同的哈希码,这是完全允许的。
    • 处理:当哈希表发现两个对象的哈希码相同时,它会调用 equals() 方法来进一步、更精确地判断这两个对象是否真的相等。equals() 返回 false,说明这是一个“哈希冲突”,哈希表会通过链表或红黑树等方式将这两个对象放在同一个桶中。
  3. a.equals(b)falsea.hashCode()b.hashCode() 最好不相等。

    • 解释:虽然契约不强制要求,但如果两个不相等的对象有相同的哈希码,它们就会被放进同一个桶里,这会导致哈希表的性能下降,因为查找元素时需要遍历桶中的链表或红黑树,时间复杂度从接近 O(1) 退化为 O(n)。
    • 目标:一个好的 hashCode() 实现应该尽量为不相等的对象生成不同的哈希码,以最小化哈希冲突,从而保持哈希表的高效性。

图示说明:

       +---------------------+
       |      HashMap        |
       +---------------------+
              /     |     \
             /      |      \
       (bucket 1) (bucket 2) (bucket 3) ...
            |        |        |
       +----+----+  +----+----+  +----+----+
       | Node A  |  | Node B  |  | Node C  |
       +----+----+  +----+----+  +----+----+
            |        |        |
      (hashCode=101) (hashCode=202) (hashCode=101)
查找过程:
1. 计算 key.hashCode() -> 得到 101。
2. 去哈希表中查找 hashCode=101 的桶。
3. 找到 bucket 1。
4. 遍历 bucket 1 中的节点,调用 key.equals(Node.key)。
   - 如果找到相等的,则返回对应的值。
   - 如果遍历完都不相等,则返回 null。

为什么需要 hashCode()?—— 性能!

hashCode() 的主要目的是为了提高性能

想象一下,如果没有 hashCode()HashMap 是如何工作的? 它只能通过遍历所有的 key,然后对每个 key 调用 equals() 方法来查找你想要的那个,对于一个有 10,000 个元素的 HashMap,最坏情况下需要比较 10,000 次,时间复杂度是 O(n)

有了 hashCode() 之后:

  1. HashMap 根据 keyhashCode() 值,可以立刻计算出这个 key 应该存储在哪个“桶”里。
  2. 它不需要遍历整个 HashMap,只需要去这一个特定的桶里查找。
  3. 如果哈希函数设计得好,每个桶里的元素很少,那么查找就非常快,平均时间复杂度是 O(1)

hashCode() 是哈希表能够实现快速查找的基石。


如何正确地重写 hashCode()

当你创建一个自定义的类,并且这个类的对象需要被放入 HashSetHashMapHashtable 中,或者你需要在 List 中查找对象时,你必须同时重写 equals()hashCode() 方法。

手动重写的步骤(不推荐,易出错):

一个简单的哈希码可以这样计算:

@Override
public int hashCode() {
    int result = 17; // 一个非零的初始值
    result = 31 * result + (name != null ? name.hashCode() : 0);
    result = 31 * result + age;
    // ... 对其他字段进行同样的操作
    return result;
}
  • 初始值:通常选择一个非零的奇数(如 17, 31),17 是一个传统选择。
  • 乘法因子:通常选择一个非零的奇数(如 31),31 是一个好选择,因为它是一个质数,31 * i 可以被优化为 (i << 5) - i,这在 JVM 层面效率很高。
  • 组合字段:将每个字段的哈希码组合起来,使用 31 * result + field.hashCode() 的公式。
  • 处理 null:如果字段可能为 null,需要处理 NullPointerException

自动生成(强烈推荐)

手动编写既繁琐又容易出错,现代的 IDE(如 IntelliJ IDEA, Eclipse)和构建工具(如 Lombok)可以一键为你生成这两个方法。

在 IntelliJ IDEA 中:

  1. 在类代码编辑区右键。
  2. 选择 Generate (或 Alt + Insert)。
  3. 选择 equals() and hashCode()
  4. IDE 会弹出一个窗口,让你选择要包含在计算中的字段,然后自动生成符合契约的代码。

使用 Lombok(更简洁): 只需在类上添加 @Data@EqualsAndHashCode 注解,Lombok 会自动生成 equals(), hashCode()toString() 方法。

import lombok.Data;
@Data
public class Person {
    private String name;
    private int age;
    // ...
}

最佳实践和常见陷阱

陷阱 1:只重写 equals(),不重写 hashCode()

这是最常见的错误,如果你把这样的对象放进 HashSetHashMap,会导致严重问题。

// 错误示例
public class Person {
    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 Objects.equals(name, person.name);
    }
    // 忘记重写 hashCode()!
}
// 后果
Set<Person> set = new HashSet<>();
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
set.add(p1);
System.out.println(set.contains(p2)); // 输出 false!
// 因为 p1.hashCode() 和 p2.hashCode() 是 Object 类的默认实现,
// 基于内存地址,所以不同,contains 方法直接根据哈希码去查找,找不到。

陷阱 2:在 hashCode() 中使用了易变对象

如果你的哈希码依赖于一个可变的字段,那么当你修改了该字段后,对象的哈希码就变了,如果这个对象已经在 HashSetHashMap 的键(key)中,你将再也找不到它了。

// 危险示例
public class MutableKey {
    private int value;
    public MutableKey(int value) {
        this.value = value;
    }
    @Override
    public int hashCode() {
        return value; // 哈希码依赖于 value
    }
    @Override
    public boolean equals(Object o) {
        // ...
    }
    public void setValue(int value) {
        this.value = value; // value 是可变的
    }
}
// 后果
Map<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey(100);
map.put(key, "Hello");
key.setValue(200); // 修改了 key 的状态
System.out.println(map.get(key)); // 输出 null!
// 因为现在 key 的哈希码是 200,而它在 map 中存储时用的哈希码是 100。
// map 在查找时,用新的哈希码 200 找不到旧的位置。

解决方案:永远不要将可变对象作为 HashMapHashSet 的键,如果必须使用,请确保键对象是不可变的(final 字段)。


Object 类的默认 hashCode() 实现

如果你没有重写 hashCode() 方法,那么你的类将使用 Object 类提供的默认实现。

这个默认实现是根据对象的内存地址来生成一个哈希码的,这意味着,即使两个对象的内容完全相同,只要它们是两个不同的实例(内存地址不同),它们的哈希码就不同。

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
System.out.println(p1.hashCode()); // 输出一个基于内存地址的哈希码
System.out.println(p2.hashCode()); // 输出另一个不同的哈希码
System.out.println(p1.equals(p2)); // 取决于你是否重写了 equals,如果没重写,结果是 false

特性 描述
定义 Object 类的方法,返回一个整数值,作为对象的哈希码。
核心契约 equals() 相等的对象,hashCode() 必须相等。
主要目的 为哈希表(HashMap, HashSet 等)提供快速查找机制,将时间复杂度从 O(n) 优化到 O(1)。
重写时机 当你的类对象需要被用作 HashMap/HashSet 的键,或者需要在集合中进行精确查找时,必须同时重写 equals()hashCode()
如何重写 强烈推荐使用 IDE 或 Lombok 自动生成,确保正确性和效率。
常见错误 只重写 equals() 而忘记重写 hashCode();在 hashCode() 中使用可变字段。
默认实现 基于对象的内存地址,不同实例的哈希码不同。

掌握 hashCode() 方法是迈向 Java 高级编程的重要一步,它深刻体现了 Java 在内存管理和算法效率上的精妙设计。

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