杰瑞科技汇

Java重写hashCode需遵循哪些原则?

这是一个在 Java 面试和日常开发中都非常重要的知识点,因为重写 hashCode() 必须与重写 equals() 方法一起进行,否则会导致严重的问题。

Java重写hashCode需遵循哪些原则?-图1
(图片来源网络,侵删)

为什么需要重写 hashCode()

我们要明白 hashCode() 方法是做什么的,它的主要作用有两个:

  1. 哈希表(如 HashMap, HashSet, Hashtable)的“路由”:当向 HashMap 这样的集合中添加一个对象时,HashMap 会先调用这个对象的 hashCode() 方法,得到一个哈希码(一个整数)。HashMap 内部会通过这个哈希码计算出对象应该被存放在哪个“桶”(Bucket)里,这使得查找和插入操作的平均时间复杂度接近 O(1),极大地提高了效率。
  2. 作为对象的“数字指纹”hashCode() 返回一个整数值,可以看作是这个对象在内存中的“指纹”,理想情况下,两个不同的对象应该有不同的“指纹”。

hashCode()equals() 的“契约”

这是最核心、最必须遵守的规则,Java 官方文档(Object 类的 hashCode() 方法的规范)定义了以下契约:

只要 equals() 方法比较的两个对象是“相等”的,那么这两个对象的 hashCode() 方法必须返回相同的整数值。

注意:反过来说不成立,如果两个对象的 hashCode() 值相同,它们不一定相等(这被称为“哈希碰撞”),如果两个对象的 hashCode() 值不同,那么它们一定不相等。

Java重写hashCode需遵循哪些原则?-图2
(图片来源网络,侵删)

这个契约意味着:

  • 对象相等,哈希码必须相等a.equals(b) 返回 truea.hashCode() 必须等于 b.hashCode(),这是强制要求,不能违反。
  • 哈希码相等,对象不一定相等a.hashCode() 等于 b.hashCode()a.equals(b) 可能返回 true,也可能返回 false

违反契约的后果

如果你只重写了 equals() 而没有重写 hashCode(),或者反之,会发生什么?

场景:将一个自定义对象 Person 放入 HashSet 中。

Java重写hashCode需遵循哪些原则?-图3
(图片来源网络,侵删)
// 假设 Person 类只重写了 equals(),没有重写 hashCode()
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
// p1.equals(p2) 应该返回 true
System.out.println(p1.equals(p2)); // true
HashSet<Person> set = new HashSet<>();
set.add(p1);
System.out.println("Set size after adding p1: " + set.size()); // 1
// 因为 p1.equals(p2) 是 true,我们期望 p2 不会被添加,set 大小仍为 1
set.add(p2);
System.out.println("Set size after adding p2: " + set.size()); // !!! 问题来了,这里可能会是 2 !!!

为什么会这样?

  1. set.add(p1) 时,HashSet 会计算 p1.hashCode() 得到值 H1,然后根据 H1p1 放入某个桶中。
  2. set.add(p2) 时,HashSet 会计算 p2.hashCode(),由于 Person 没有重写 hashCode(),它调用的是 Object 类的 hashCode() 方法,该方法基于对象的内存地址。p1p2 是两个不同的对象实例,所以它们的内存地址不同,p1.hashCode()p2.hashCode() 的值也不同
  3. HashSet 发现哈希码不同,就认为 p1p2 是两个完全不同的对象,于是将 p2 放入了另一个桶中。
  4. set 中就包含了两个我们认为“相等”的对象,这显然是错误的行为。

为了保证基于哈希的集合(HashMap, HashSet 等)的正确性,一旦你重写了 equals(),就必须重写 hashCode()


如何正确地重写 hashCode()

重写 hashCode() 的目标是:尽可能让 equals() 返回 true 的对象拥有相同的 hashCode,同时让不同的对象拥有不同的 hashCode,以减少哈希碰撞。

手动实现(不推荐,但有助于理解)

你需要选择一个算法,将对象的“关键属性”组合成一个整数,一个经典且有效的算法是:

  1. 声明一个非零的常量作为哈希码的初始值(17)。
  2. 为每个参与 equals() 比较的关键属性计算一个“哈希码因子”。
  3. 将初始值与每个属性的哈希码因子进行“异或”运算,并乘以一个质数(31)。
  4. 返回最终结果。

为什么用 31

  • 31 是一个质数,能减少哈希碰撞的概率。
  • 31 * i 可以被优化为 (i << 5) - i,即 i * 32 - i,这种位运算在 JVM 上非常快。

示例:重写 Person 类的 hashCode()

假设 Person 类有两个关键属性:name (String) 和 age (int)。

public class Person {
    private String name;
    private int age;
    // 构造函数、getters、setters 省略...
    @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);
    }
    @Override
    public int hashCode() {
        // 1. 声明一个非零常量
        int result = 17;
        // 2. 为每个关键属性计算哈希码因子并组合
        // 31 是一个常用的质数
        result = 31 * result + (name == null ? 0 : name.hashCode());
        result = 31 * result + age;
        return result;
    }
}

使用 IDE 自动生成(推荐)

在实际开发中,手动编写容易出错,IDE 非常擅长生成符合规范的代码,这是最常用、最可靠的方法。

以 IntelliJ IDEA 为例:

  1. 在类中右键 -> Generate (或按 Alt + Insert)。
  2. 选择 equals() and hashCode()
  3. IDE 会弹出一个窗口,让你选择要包含在 equals()hashCode() 计算中的字段,确保你选择的字段与 equals() 方法中使用的字段一致。
  4. 点击 OK,IDE 就会自动生成高质量的代码。

IDE 生成的代码通常和手动实现的方法类似,但更简洁,使用了 java.util.Objects 工具类。

IDE 生成的代码示例:

import java.util.Objects;
public class Person {
    private String name;
    private int age;
    // ... 其他代码 ...
    @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);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Objects.hash() 的优点:

  • 简洁:一行代码搞定。
  • 安全:自动处理了 null 值,无需像手动实现那样写 field == null ? 0 : field.hashCode()
  • 高效:内部也使用了类似的质数乘法算法来组合哈希码。

注意:虽然 Objects.hash() 非常方便,但如果你对性能有极致要求,并且你的对象是“热对象”(被频繁创建和使用),手动实现可能会因为避免了数组的创建而有一点点性能优势,但在绝大多数场景下,Objects.hash() 的便利性远胜于那点微不足道的性能差异。

使用 Lombok(如果项目允许)

如果你的项目中使用了 Lombok 库,事情就变得更简单了。

只需在类上添加 @EqualsAndHashCode 注解即可。

import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class Person {
    private String name;
    private int age;
    // ... 其他代码 ...
}

Lombok 会在编译时自动为你生成 equals()hashCode() 方法,其逻辑和 IDE 生成的类似,这是目前最省事的方式。


重写 hashCode() 的最佳实践

  1. 始终与 equals() 一起重写:这是铁律。
  2. 使用关键属性:参与 hashCode() 计算的字段列表,必须与参与 equals() 计算的字段列表完全一致
  3. 优先使用 IDE 或 Lombok:不要手动编写,除非有特殊需求或出于学习目的。
  4. 考虑不可变性:如果一个对象是不可变的(创建后其属性不能被修改),那么它的 hashCode 只需计算一次并缓存起来,这能提高性能,在 final 字段中缓存哈希码。
  5. 不要过度优化:不要为了追求完美的哈希分布而引入复杂的计算,一个好的哈希函数的目标是“足够好”,而不是“完美”,简单的质数乘法算法已经足够高效。

问题 答案
为什么要重写? 为了让 equals() 相等的对象在 HashMap, HashSet 等哈希集合中被视为同一个对象,保证集合逻辑的正确性。
equals() 的关系? 契约:equals()true 的对象,hashCode() 必须相等。
如何重写? 推荐:使用 IDE 的 Generate equals() and hashCode() 功能,或使用 Lombok 的 @EqualsAndHashCode 注解。
核心原则? hashCode() 的计算字段必须与 equals() 的计算字段完全一致

掌握了这些,你就能在 Java 开发中正确、自信地处理 hashCode() 的重写问题了。

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