杰瑞科技汇

Java中equals重写要注意哪些关键点?

这是一个在 Java 编程中至关重要且容易出错的话题,理解并正确地重写 equals() 方法对于编写健壮、可预测的代码至关重要。

Java中equals重写要注意哪些关键点?-图1
(图片来源网络,侵删)

为什么需要重写 equals()

我们要明白 equals() 的默认行为是什么。

在 Java 中,所有类都直接或间接继承自 Object 类。Object 类中提供的 equals() 方法的默认实现是:

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

这个实现比较的是两个对象的内存地址(即引用),只有当两个引用指向同一个对象时(thisobj 是同一个东西),它才返回 true

问题场景: 考虑一个 Person 类,我们希望两个 Person 对象如果拥有相同的 nameage,就认为它们是“相等”的,即使它们是两个不同的实例(内存地址不同)。

Java中equals重写要注意哪些关键点?-图2
(图片来源网络,侵删)
Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25);
// 使用 Object 默认的 equals()
System.out.println(p1.equals(p2)); // 输出 false,因为 p1 和 p2 是两个不同的对象

显然,这不符合我们的业务逻辑,我们需要重写 equals() 方法,来定义我们自己的“相等”标准。


重写 equals() 的正确方法(黄金法则)

要正确地重写 equals() 方法,你必须遵循 Java 官方文档(来自 Object 类的 equals 方法的契约)中的所有约定,否则,你的类在集合(如 HashSet, HashMap)中可能无法正常工作,或者与其他依赖 equals() 方法的 API 产生不可预测的行为。

以下是必须遵循的五大黄金法则

  1. 自反性:对于任何非 null 的引用值 xx.equals(x) 必须返回 true
  2. 对称性:对于任何非 null 的引用值 xyx.equals(y) 必须与 y.equals(x) 具有相同的返回值。
  3. 传递性:对于任何非 null 的引用值 xyzx.equals(y) 返回 true y.equals(z) 返回 truex.equals(z) 也必须返回 true
  4. 一致性:对于任何非 null 的引用值 xy,只要对象上用于 equals 比较的信息没有被修改,多次调用 x.equals(y) 必须一致地返回 truefalse
  5. 非空性:对于任何非 null 的引用值 xx.equals(null) 必须返回 false

手动重写 equals() 的标准步骤

遵循上述原则,我们可以总结出一个安全、可靠的重写步骤,我们以一个 Student 类为例:

Java中equals重写要注意哪些关键点?-图3
(图片来源网络,侵删)
public class Student {
    private String studentId;
    private String name;
    private int age;
    // 构造函数、getters 和 setters...
    public Student(String studentId, String name, int age) {
        this.studentId = studentId;
        this.name = name;
        this.age = age;
    }
    // ... getters and setters
}

检查“是否为同一个对象”

使用 操作符检查比较的引用是否是同一个对象,如果是,直接返回 true,这可以快速判断,提高性能,也满足了自反性。

@Override
public boolean equals(Object o) {
    // 1. 检查是否为同一个对象
    if (this == o) {
        return true;
    }

检查“参数是否为 null 或类型是否匹配”

如果参数 onull,或者 o 的类型不是 Student,那么它们肯定不相等,使用 instanceof 操作符进行类型检查,这满足了非空性。

    // 2. 检查参数是否为 null 或类型是否匹配
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
  • getClass() != o.getClass() 是比 instanceof 更严格的检查,它确保了子类不会与父类“相等”,除非你明确希望这样,对于大多数情况,这是更好的选择。

进行类型转换

既然通过了类型检查,我们就可以将 Object 类型的参数 o 安全地转换为 Student 类型。

    // 3. 将对象进行类型转换
    Student student = (Student) o;

比较“关键字段”

比较决定对象“相等性”的关键字段,对于基本类型,使用 ;对于对象类型,使用 Objects.equals()String.equals()(避免 NullPointerException),我们比较所有字段,但也可以根据业务逻辑选择性地比较。

    // 4. 比较关键字段
    // 使用 Objects.equals 可以安全地处理可能为 null 的字段
    return Objects.equals(studentId, student.studentId) &&
           Objects.equals(name, student.name) &&
           age == student.age;
}
  • 为什么使用 Objects.equals() 因为它可以安全地处理 null 值,避免了 NullPointerExceptionObjects.equals(a, b) 内部实现是 a == b || (a != null && a.equals(b))

完整的 equals() 方法示例

将以上步骤整合起来,我们得到一个完整且健壮的 equals() 方法:

import java.util.Objects;
public class Student {
    private String studentId;
    private String name;
    private int age;
    // 构造函数、getters 和 setters...
    public Student(String studentId, String name, int age) {
        this.studentId = studentId;
        this.name = name;
        this.age = age;
    }
    // ... getters and setters
    /**
     * 重写 equals 方法
     */
    @Override
    public boolean equals(Object o) {
        // 1. 检查是否为同一个对象
        if (this == o) return true;
        // 2. 检查参数是否为 null 或类型是否匹配
        if (o == null || getClass() != o.getClass()) return false;
        // 3. 将对象进行类型转换
        Student student = (Student) o;
        // 4. 比较关键字段
        return age == student.age &&
               Objects.equals(studentId, student.studentId) &&
               Objects.equals(name, student.name);
    }
    // ... hashCode() 方法(下面会讲)
}

必须同时重写 hashCode() 方法

这是最容易被忽略,但又至关重要的一点。

hashCode() 约定:

  • 在 Java 应用程序的一次执行期间,只要对象的 equals 方法所用的信息没有被修改,那么对同一个对象多次调用 hashCode 方法,必须一致地返回同一个整数。
  • 如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象的 hashCode 方法必须产生相同的整数结果。
  • 如果两个对象根据 equals(Object) 方法是不相等的,那么对这两个对象调用 hashCode 方法,不要求一定产生不同的整数结果。(程序员应该知道,为不相等的对象生成不同的 hashCode 可以提高哈希表的性能。)

为什么必须同时重写? 因为 Java 中许多基于哈希的集合类(如 HashSet, HashMap, Hashtable)都依赖这两个方法。

工作原理:

  1. 当你向 HashSet 中添加一个对象时,HashSet 首先会调用该对象的 hashCode() 方法,计算出其哈希码,并确定一个“桶”(bucket)的位置。
  2. 如果该“桶”为空,就直接将对象存入。
  3. 如果该“桶”不为空,HashSet 会遍历该“桶”中的所有对象,并使用 equals() 方法与新对象进行比较。
    • equals() 返回 true,说明对象已存在,HashSet 不会重复添加。
    • equals() 返回 false,说明这是一个“哈希冲突”(Hash Collision),新对象会被作为链表或红黑树节点添加到这个“桶”中。

错误示范: 如果你只重写了 equals() 而没有重写 hashCode(),会发生什么?

Student s1 = new Student("101", "Alice", 20);
Student s2 = new Student("101", "Alice", 20);
HashSet<Student> set = new HashSet<>();
set.add(s1);
System.out.println("集合中是否包含 s2? " + set.contains(s2)); // 可能输出 false!
  • s1s2 通过 equals() 比算是相等的。
  • s1s2 是两个不同的对象,它们的默认 hashCode() 值(基于内存地址)不同。
  • s1 被加入 HashSet 时,它被存入根据其 hashCode 计算出的“桶” A。
  • set.contains(s2) 被调用时,HashSet 计算 s2hashCode,发现它指向了不同的“桶” B,于是直接返回 false,甚至不会去调用 equals() 方法进行比较。

如果两个对象相等,它们的 hashCode 必须相等,一旦你重写了 equals(),就必须同时重写 hashCode()

如何重写 hashCode()

最简单、最推荐的方式是使用 IDE(如 IntelliJ IDEA 或 Eclipse)自动生成,它们生成的代码非常标准和高效。

手动重写时,可以使用 Objects.hash() 方法,它会帮你处理所有细节:

@Override
public int hashCode() {
    return Objects.hash(studentId, name, age);
}

这个方法内部会为每个传入的参数计算哈希码,并将它们组合起来,完全符合 hashCode() 的约定。


最佳实践与总结

  1. 何时重写? 当你的类需要基于业务逻辑判断对象是否“相等”,并且该类会被用作 HashMapHashSetHashtable 的键,或者被放入这些集合中时,必须重写 equals()hashCode()
  2. 不要重写:如果类的“相等性”永远基于内存地址(即 this == obj),或者类是不可变的,并且你确定它永远不会被用于哈希集合,那么你可以不重写(使用 Object 的默认实现)。
  3. 使用 IDE:强烈建议使用 IDE 的自动生成功能(IntelliJ 中是 Code -> Generate -> equals() and hashCode()),这可以避免手写错误,并保持代码的一致性。
  4. 字段一致性equals()hashCode() 中比较的字段必须完全一致,如果在 equals() 中比较了 nameage,那么在 hashCode() 中也必须使用 nameage 来计算哈希码。
  5. 性能:将最有可能不相同的、计算成本低的字段放在 equals() 比较的前面,可以更快地返回 false,提高性能。

通过遵循这些规则,你可以确保你的类行为符合 Java 的约定,在各种场景下都能正常工作。

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