杰瑞科技汇

为何要重写Java的hashCode方法?

为什么需要重写 hashCode()

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

为何要重写Java的hashCode方法?-图1
(图片来源网络,侵删)

hashCode() 的作用

hashCode() 方法返回一个整数,这个整数被称为“哈希码”或“散列码”,它的主要作用是:

  1. 支持哈希表数据结构:Java 中最重要的哈希表数据结构就是 HashMapHashSetHashtable,这些数据结构通过哈希码来决定对象应该存储在“桶”(Bucket)中的哪个位置,从而实现数据的快速存取(平均时间复杂度为 O(1))。
  2. 快速判断对象“可能”相等:当两个对象的哈希码不相等时,我们可以100%确定这两个对象不相等,但如果哈希码相等,这两个对象不一定相等(这被称为“哈希冲突”或“哈希碰撞”)。

equals()hashCode() 的“契约”

这是最核心的部分,Java 官方文档中规定,任何重写了 equals() 方法的类,都必须同时重写 hashCode() 方法,它们之间必须遵守以下三条“契约”:

  1. 一致性:如果两个对象根据 equals() 方法是相等的,那么调用这两个对象的 hashCode() 方法必须产生相同的整数结果。
    • a.equals(b)true,则 a.hashCode() == b.hashCode() 必须为 true
  2. 对称性:如果两个对象根据 equals() 方法是不相等的,那么调用这两个对象的 hashCode() 方法不要求产生不同的整数结果,为了提高哈希表的性能,强烈推荐为不相等的对象生成不同的哈希码。
    • a.equals(b)false,则 a.hashCode() != b.hashCode() 最好true
    • 如果不相等的对象产生了相同的哈希码,就会导致哈希冲突,降低哈希表的效率。
  3. 稳定性:在一个应用程序的多次执行过程中,同一个对象的 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 失效。

为何要重写Java的hashCode方法?-图2
(图片来源网络,侵删)

如何正确地重写 hashCode()

重写 hashCode() 的目标是:尽可能为不相等的对象生成不同的哈希码,同时保证相等的对象哈希码相同。

Java 提供了两种主流的重写方式。

手动计算(不推荐,但有助于理解)

核心思想是将对象中所有参与 equals() 比较的字段的哈希码进行组合。

  1. 定义一个非零的初始值(17)。
  2. 对于每一个 equals() 中用到的字段,执行以下操作:
    • result = 31 * result + (field == null ? 0 : field.hashCode())
  3. 返回 result

为什么用 31?

  • 31 是一个奇质数,质数能减少哈希冲突的概率。
  • 31 * i 可以被优化为 (i << 5) - i,即 i * 32 - i,这种位运算在现代 JVM 上比乘法运算更快。

示例: 假设我们有一个 Person 类,equals() 基于 idname

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 操作

  1. 在类中右键 -> Generate (或 Alt + Insert)。
  2. 选择 equals() and hashCode()
  3. 勾选所有参与比较的字段,点击 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() 的最佳实践

  1. 总是和 equals() 成对出现:如果你修改了 equals(),确保同时修改 hashCode(),反之亦然。
  2. 使用 IDE 的自动生成功能:这是最可靠、最高效的方式,可以避免人为错误。
  3. 只使用 equals() 中用到的字段hashCode() 的计算字段必须与 equals() 的比较字段保持一致,否则会破坏第一条契约。
  4. 处理 null 字段:如果字段可能为 null,调用 field.hashCode() 会抛出 NullPointerException,可以使用 field == null ? 0 : field.hashCode() 或者 Objects.hash() 会自动处理 null
  5. 选择好的哈希算法:目标是让哈希码尽可能均匀地分布,以减少哈希冲突,简单的 31 乘法对大多数场景已经足够,如果对象有多个字段,特别是数组或集合,可以考虑更复杂的算法,如 java.util.Arrays.hashCode()java.util.Collections.hashCode()
  6. 保持高性能hashCode() 方法应该非常快,避免在其中进行复杂的计算或 I/O 操作。

特性 描述
核心目的 为了与 equals() 方法保持一致,确保 HashMap 等哈希表结构能正确工作。
核心契约 相等的对象必须有相同的哈希码。
常见错误 只重写 equals() 而忘记重写 hashCode()
推荐实践 使用 IDE(如 IntelliJ IDEA)自动生成 equals()hashCode() 方法。
工具类 Objects.hash() 是生成哈希码的便捷工具。
关键字 equals(), hashCode(), HashMap, HashSet, 契约, 哈希冲突

掌握 hashCode() 的重写是成为一名合格 Java 开发者的必经之路,记住它与 equals() 的紧密关系,并善用 IDE 工具,你就能轻松应对这个问题。

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