杰瑞科技汇

Java对象equals方法如何正确重写?

目录

  1. equals() 方法的定义与目的
  2. Object 类中的 equals() 方法
  3. 为什么需要重写 equals() 方法?
  4. 如何正确地重写 equals() 方法?(黄金法则)
  5. 重写 equals() 的完整示例
  6. equals() 与 的区别(终极对比)
  7. 重写 equals() 时必须重写 hashCode()
  8. 总结与最佳实践

equals() 方法的定义与目的

equals() 方法是 java.lang.Object 类中的一个方法,它的核心目的是判断两个对象是否“相等”

Java对象equals方法如何正确重写?-图1
(图片来源网络,侵删)

Object 类中提供的默认实现,对于判断“相等”的定义可能并不符合我们业务逻辑的需求,我们经常需要在自己的类中重写(Override)这个方法。


Object 类中的 equals() 方法

如果你不重写 equals() 方法,那么你的类就会继承 Object 类中的默认实现,我们来看一下它的源码:

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

这个默认实现非常简单:它只做了一件事,就是比较两个对象的内存地址是否相同。

  • this == obj:这里的 比较的是两个引用变量是否指向堆内存中的同一个对象实例(即内存地址是否相同)。
  • 返回值:如果指向同一个对象,返回 true;否则返回 false

示例:

Java对象equals方法如何正确重写?-图2
(图片来源网络,侵删)
public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public static void main(String[] args) {
        Person p1 = new Person("Alice");
        Person p2 = new Person("Alice");
        Person p3 = p1;
        System.out.println(p1.equals(p2)); // 输出 false,因为 p1 和 p2 是两个不同的对象实例
        System.out.println(p1.equals(p3)); // 输出 true,因为 p3 和 p1 指向同一个对象实例
    }
}

在这个例子中,p1p2 虽然内容(name 属性)相同,但它们是 new 出来的两个独立对象,内存地址不同,p1.equals(p2) 返回 false,这通常不是我们想要的结果。


为什么需要重写 equals() 方法?

我们重写 equals() 方法,是为了改变“相等”的定义,我们希望根据对象“状态”(即对象的属性值)来判断它们是否相等,而不是根据它们的内存地址。

继续上面的 Person 类例子: 我们更希望认为两个 name 相同的 Person 对象是“相等”的,我们需要重写 equals() 方法。


如何正确地重写 equals() 方法?(黄金法则)

为了确保你的 equals() 方法行为正确、健壮,并且能被集合框架(如 HashMap, HashSet)正确使用,必须遵循以下黄金法则(通常指 Joshua Bloch 在《Effective Java》中提出的规范):

Java对象equals方法如何正确重写?-图3
(图片来源网络,侵删)
  1. 自反性:对于任何非 null 的引用值 xx.equals(x) 必须返回 true
  2. 对称性:对于任何非 null 的引用值 xyx.equals(y) 返回 truey.equals(x) 也必须返回 true
  3. 传递性:对于任何非 null 的引用值 xyzx.equals(y) 返回 truey.equals(z) 也返回 truex.equals(z) 也必须返回 true
  4. 一致性:对于任何非 null 的引用值 xy,只要 xy 所包含的信息没有被修改,多次调用 x.equals(y) 必须一致地返回 truefalse
  5. 非空性:对于任何非 null 的引用值 xx.equals(null) 必须返回 false

推荐的 equals() 方法重写模板:

@Override
public boolean equals(Object o) {
    // 1. 检查是否是同一个对象的引用(地址相同)
    if (this == o) return true;
    // 2. 检查参数是否为 null,或者是否属于不同的类
    // 使用 getClass() 是更严格的检查,它要求类型完全一致
    if (o == null || getClass() != o.getClass()) return false;
    // 3. 将对象进行类型转换
    MyClass that = (MyClass) o;
    // 4. 比较类的“关键属性”
    // 使用 Objects.equals() 可以安全地处理可能为 null 的属性
    // 使用 == 比较基本类型
    return Objects.equals(this.field1, that.field1) &&
           this.field2 == that.field2;
    // ... 继续比较其他重要属性
}

getClass()instanceof 的选择:

  • getClass():这种方式更严格,它要求两个对象必须是同一个类的实例才可能相等。class B extends A,一个 A 的实例和一个 B 的实例即使内容完全一样,equals 也会返回 false
  • instanceof:这种方式更宽松,它允许一个类的实例和其子类的实例进行比较,如果使用 instanceof,你需要确保子类不会破坏父类的相等性契约(这通常很复杂)。

通常推荐使用 getClass(),因为它更简单、更安全,也更符合“类型相同才能相等”的直觉。


重写 equals() 的完整示例

我们来重写 Person 类的 equals() 方法,使其根据 name 判断是否相等。

import java.util.Objects;
public class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // --- 重写 equals() 方法 ---
    @Override
    public boolean equals(Object o) {
        // 1. 自反性检查
        if (this == o) return true;
        // 2. 检查 null 和类型
        if (o == null || getClass() != o.getClass()) return false;
        // 3. 类型转换
        Person person = (Person) o;
        // 4. 比较关键属性
        // 我们认为 name 是关键属性,age 不是
        return Objects.equals(name, person.name);
    }
    // --- 为了完整性,也重写 toString() 方便打印 ---
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 25); // age 不同
        Person p3 = new Person("Bob", 30);
        Person p4 = p1;
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true, 因为 name 都是 "Alice"
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // false, 因为 name 不同
        System.out.println("p1.equals(p4): " + p1.equals(p4)); // true, 因为是同一个对象
        System.out.println("p1.equals(null): " + p1.equals(null)); // false
    }
}

输出:

p1.equals(p2): true
p1.equals(p3): false
p1.equals(p4): true
p1.equals(null): false

这个结果完全符合我们的预期。


equals() 与 的区别(终极对比)

这是 Java 面试中最经典的问题之一。

特性 equals()
基本类型 比较值,比较两个变量存储的数值是否相等。 不能使用equals() 是方法,基本类型变量没有方法。
引用类型 比较引用(内存地址),判断两个变量是否指向堆中的同一个对象。 默认比较引用(与 相同),但可以被重写,用于比较对象的内容(状态)。
示例 int a = 5; int b = 5; a == b; // true
Person p1 = new Person(); Person p2 = new Person(); p1 == p2; // false
p1.equals(p2); // 默认为 false,取决于是否重写及重写逻辑

记忆口诀:

  • 基本类型比值,引用类型比地址。
  • equals() 默认比地址,但通常被重写来比内容。

重写 equals() 时必须重写 `hashCode()**

这是一个非常重要的约定,如果你违反了它,你的对象将无法在基于哈希的集合(如 HashMap, HashSet, Hashtable)中正常工作。 **

如果两个对象根据 equals() 方法是相等的,那么调用这两个对象中任意一个对象的 hashCode() 方法都必须产生相同的整数结果。

为什么需要这个约定?

让我们看看 HashMap 的工作原理:

  1. 当你将一个键值对存入 HashMap 时,HashMap 会先计算键对象的 hashCode() 得到一个哈希值。
  2. 根据这个哈希值,找到 HashMap 底层数组中的某个位置(桶)。
  3. 它会用 equals() 方法去比较这个桶中的元素和你的新键是否相等。
    • equals() 返回 true,说明是同一个键,会覆盖旧值。
    • equals() 返回 false,说明发生了哈希冲突,会将新元素以链表或红黑树的形式挂在这个桶后面。

如果只重写 equals() 而不重写 hashCode() 会发生什么?

假设你定义了一个 Person 类,重写了 equals() 方法(根据 name 判断相等),但没有重写 hashCode(),那么它会使用 Object 类的默认 hashCode() 实现(基于内存地址)。

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
// p1.equals(p2) 返回 true
// p1.hashCode() 和 p2.hashCode() 几乎肯定不相等(因为它们是不同对象)
Map<Person, String> map = new HashMap<>();
map.put(p1, "First Alice");
// 现在尝试用 p2 作为 key 获取值
String value = map.get(p2); // 会返回 null!

原因分析:

  1. map.put(p1, ...):计算 p1.hashCode(),假设得到 100,将 (p1, "First Alice") 存入哈希表 100 号桶。
  2. map.get(p2):计算 p2.hashCode(),假设得到 200,直接去哈希表 200 号桶查找,结果 200 号桶是空的,所以返回 null,它根本没有机会去调用 p2.equals(p1)

为了保证基于哈希的集合能够正确工作,你必须遵守这个约定:“相等的对象必须有相同的哈希码”

如何重写 hashCode() 最简单、最推荐的方式是使用 Objects.hash() 方法:

@Override
public int hashCode() {
    // 将你认为对“相等性”有贡献的属性组合起来
    return Objects.hash(name);
}

Objects.hash() 内部会处理 null 值,并为多个属性生成一个合理的哈希码。


总结与最佳实践

  1. 理解默认行为Objectequals() 比较内存地址。
  2. 明确需求:当你需要根据对象内容判断相等时,就必须重写 equals()
  3. 遵循契约:重写 equals() 时,必须严格遵守自反、对称、传递、一致和非空五大原则。
  4. 使用模板:推荐使用 getClass() + Objects.equals() 的模板来安全地重写 equals()
  5. 黄金法则永远、永远、永远在重写 equals() 的同时重写 hashCode(),并确保“相等的对象有相同的哈希码”。
  6. IDE 辅助:现代 IDE(如 IntelliJ IDEA, Eclipse)可以一键生成符合规范的 equals()hashCode() 方法,强烈推荐使用,而不是手动编写。
分享:
扫描分享到社交APP
上一篇
下一篇