基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

重写equals与hashCode详解

知识点图片

本文讲解了重写equals()方法以实现对象内容的逻辑相等性,并强调必须同时重写hashCode()。文章提供了遵循五个协定(自反、对称、传递、一致、非空)的最佳实践模板,确保与哈希集合兼容。

为什么需要重写 equals()

在 Java 中,所有类都隐式或显式地继承自 java.lang.Object 类。Object 类提供了一个基本的 equals() 方法,其默认实现是:

java
public boolean equals(Object obj) {
    return (this == obj);
}

这表示,默认的 equals() 方法比较的是两个对象的内存地址,即判断它们是否为同一个对象的引用。这被称为“引用相等性”(Reference Equality)。

然而,在很多业务场景下,我们关心的是两个对象的内容或状态是否相同,而不是它们是否指向同一块内存。例如,我们认为 new String("hello") 和另一个 new String("hello") 是相等的。这种基于内容的比较被称为“逻辑相等性”(Logical Equality)。

因此,当你希望自定义类的相等性判断标准时,就需要重写 equals() 方法。


equals() 方法的重写协定(The Contract)

Java 官方文档为 equals() 方法规定了五个必须遵守的通用协定。如果违反这些协定,可能会导致程序的行为变得不可预测,尤其是在使用集合类(如 HashMap, HashSet)时。

这五个协定是:

  1. 自反性 (Reflexive)

    • 规则:对于任何非 null 的引用值 xx.equals(x) 必须返回 true
    • 解释:一个对象必须等于它自己。
  2. 对称性 (Symmetric)

    • 规则:对于任何非 null 的引用值 xy,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才必须返回 true
    • 解释:如果 x 等于 y,那么 y 也必须等于 x
  3. 传递性 (Transitive)

    • 规则:对于任何非 null 的引用值 xyz,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 必须返回 true
    • 解释:如果 x 等于 yy 等于 z,那么 x 必须等于 z
  4. 一致性 (Consistent)

    • 规则:对于任何非 null 的引用值 xy,只要 equals 比较操作中使用的信息没有被修改,那么多次调用 x.equals(y) 的结果应该保持一致(要么一直返回 true,要么一直返回 false)。
    • 解释:只要对象的状态不变,equals() 的结果就不应该改变。
  5. 非空性 (Non-nullity)

    • 规则:对于任何非 null 的引用值 xx.equals(null) 必须返回 false
    • 解释:任何对象都不等于 null

hashCode() 的重要规则

这是一条与 equals() 紧密相关的、至关重要的规则:

如果你重写了 equals() 方法,那么你必须重写 hashCode() 方法。

为什么?

hashCode() 用于为对象生成一个整数“哈希码”,这个值主要用于基于哈希的集合,如 HashMap, HashSet, Hashtable。这些集合使用哈希码来确定对象的存储位置,以实现快速查找。

hashCode() 的协定要求:

  • 如果两个对象根据 equals() 方法是相等的,那么它们的 hashCode() 方法必须返回相同(相等)的整数。
  • 如果两个对象根据 equals() 方法是不相等的,它们的 hashCode() 方法不要求必须返回不同的整数。但是,为不相等的对象生成不同的哈希码可以提高哈希表的性能。

违反此规则的后果:
如果你只重写了 equals() 而没有重写 hashCode(),那么当你把对象放入 HashSet 或作为键放入 HashMap 时,就会出现问题。

例如,你创建了两个内容相同的 Person 对象 p1p2

  • p1.equals(p2) 返回 true
  • 但由于没有重写 hashCode(),它们会使用 Object 类的默认实现,该实现通常基于内存地址,因此 p1.hashCode()p2.hashCode() 很可能不同。

当你执行 set.add(p1),然后 set.contains(p2) 时,HashSet 会先计算 p2 的哈希码来确定查找的“桶”(bucket),由于哈希码不同,它很可能找不到 p1 所在的桶,从而直接返回 false,尽管 p1p2 在逻辑上是相等的。


重写 equals() 的最佳实践步骤

这是一个经过验证的、可以安全实现 equals() 方法的模板。

java
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    // constructor, getters, setters...

    // 最佳实践步骤
    @Override
    public boolean equals(Object obj) {
        // 1. 使用 == 检查参数是否为这个对象的引用(性能优化)
        if (this == obj) {
            return true;
        }

        // 2. 检查参数是否为 null,以及类型是否匹配
        // 使用 getClass() 而不是 instanceof 可以确保比较的对象是完全相同的类,
        // 避免了子类可能带来的对称性问题。
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        // 3. 将参数转换为正确的类型
        Person person = (Person) obj;

        // 4. 对所有“关键”字段进行比较
        //    - 基本类型使用 ==
        //    - 对象类型使用 Objects.equals()(它能优雅地处理 null)
        return age == person.age &&
               Objects.equals(name, person.name);
    }
}

步骤分解:

  1. @Override 注解:强烈建议使用。它可以帮助编译器检查你是否正确地重写了方法(例如,参数类型是否为 Object)。
  2. 引用检查 (this == obj):这是一个简单的性能优化。如果两个引用指向同一个对象,它们必然是相等的。
  3. null 和类型检查
    • obj == null:符合非空性协定。
    • getClass() != obj.getClass():确保比较的两个对象属于同一个类。使用 instanceof 也可以,但在处理继承时可能破坏对称性,因此 getClass() 通常是更安全的选择。
  4. 类型转换:在通过了类型检查后,可以安全地将 Object 类型的参数强制转换为你的类类型。
  5. 字段比较:这是核心逻辑。
    • 对于 int, double, boolean 等基本数据类型,直接使用 == 比较。
    • 对于对象类型(如 String, Date 或其他自定义类),必须使用 Objects.equals(a, b)。这个静态方法是 Java 7 引入的,它会自动处理其中一个或两个对象为 null 的情况,非常安全和方便。

配套的 hashCode() 实现

与上面的 Person.equals() 方法配套的 hashCode() 实现如下:

java
@Override
public int hashCode() {
    // 使用 Objects.hash() 工具方法来为所有关键字段生成一个哈希码
    // 它能处理 null 值,并且实现方式高效、标准。
    return Objects.hash(name, age);
}

Objects.hash(Object... values) 是 Java 7 引入的便捷方法,它会根据你传入的字段计算出一个合适的哈希码。你应该将 equals() 方法中用到的所有字段都传给它。

现代 Java 的简化方式:Record (Java 14+)

如果你使用的是 Java 14 或更高版本,并且你的类主要用作不可变的数据载体,那么可以使用 Record 来极大简化这个过程。

java
// 使用 record,编译器会自动为你生成正确的 equals(), hashCode(), 和 toString() 方法
public record PersonRecord(String name, int age) {}

这短短一行代码等同于前面手动编写的、包含构造函数、getter、equals()hashCode() 的整个 Person 类。编译器生成的实现完全符合所有协定,是现代 Java 的首选方案。

总结 Checklist

在重写 equals() 时,请检查以下几点:

  • 是否遵守了五个协定? (自反、对称、传递、一致、非空)
  • 是否也重写了 hashCode()
  • equals 方法的参数类型是否为 Object
  • 是否使用了 @Override 注解?
  • 是否正确处理了 null
  • 是否正确比较了基本类型和对象类型? (推荐使用 Objects.equals()
  • 是否可以考虑使用 Record 来代替?
00:00
00:00