杰瑞科技汇

Java中如何判断两个对象是否相等?

  1. 引用相等 (Reference Equality):判断两个引用变量是否指向内存中的同一个对象。
  2. 值相等 (Value Equality):判断两个对象的内容(状态)是否相同,即使它们是内存中不同的两个对象。

下面我们来详细讲解这两种方式,以及如何正确地实现自定义对象的值相等判断。

Java中如何判断两个对象是否相等?-图1
(图片来源网络,侵删)

引用相等 ( 运算符)

这是最基本、最快速的判断方式。 比较的是两个变量在内存中的地址(引用)。

  • 规则:如果两个引用指向同一个对象实例(内存地址相同),则 返回 true;否则返回 false
  • 适用场景:通常用于判断是否为同一个对象实例。
  • 示例
public class ReferenceEqualityExample {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");
        String s3 = s1;
        // s1 和 s2 是两个不同的对象,引用地址不同
        System.out.println("s1 == s2: " + (s1 == s2)); // 输出: false
        // s1 和 s3 指向同一个对象,引用地址相同
        System.out.println("s1 == s3: " + (s1 == s3)); // 输出: true
    }
}

值相等 (equals() 方法)

当我们需要比较两个对象的内容是否相同时,应该使用 equals() 方法。equals()Object 类中的一个方法,所有 Java 类都继承自 Object

1 Object 类中的 equals()

Object 类中默认的 equals() 方法实现与 运算符完全相同,它也是比较两个对象的引用地址。

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

这意味着,如果你直接使用一个自定义类的实例来调用 equals(),它默认的行为仍然是判断引用是否相等。

Java中如何判断两个对象是否相等?-图2
(图片来源网络,侵删)

2 重写 equals() 方法

为了实现“值相等”,我们必须在自己的类中重写 (Override) equals() 方法。

重写 equals() 的黄金法则(来自《Effective Java》):

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

3 正确重写 equals() 的步骤和模板

一个健壮的 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 myClass = (MyClass) o;
    // 4. 比较类的“关键”字段
    // 使用 Objects.equals() 可以安全地处理可能为 null 的字段
    // 比较基本类型时,使用 == 即可
    return Objects.equals(this.field1, myClass.field1) &&
           this.field2 == myClass.field2 &&
           Objects.equals(this.field3, myClass.field3);
}

示例:重写 Person 类的 equals() 方法

假设我们有一个 Person 类,我们希望两个 Person 对象的 nameage 相同就认为它们相等。

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;
    }
    // Getters and Setters
    public String getName() { return name; }
    public int getAge() { return 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. 比较关键字段
        // age 是基本类型,用 ==
        // name 是引用类型,用 Objects.equals() 来处理 null 的情况
        return age == person.age &&
               Objects.equals(name, person.name);
    }
    // 重要:重写 equals() 后,强烈建议也重写 hashCode()!
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    // 为了方便调试,也建议重写 toString()
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试 equals() 方法

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        Person p3 = new Person("Bob", 30);
        Person p4 = p1;
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // 输出: true (内容相同)
        System.out.println("p1 == p2: " + (p1 == p2));        // 输出: false (不是同一个对象)
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // 输出: false (内容不同)
        System.out.println("p1.equals(p4): " + p1.equals(p4)); // 输出: true (同一个对象)
    }
}

equals()hashCode() 的契约关系

这是一个非常重要的概念,尤其当你将对象用作 HashMap 的键或 HashSet 的元素时。

契约:如果两个对象根据 equals() 方法判断是相等的,那么调用它们的 hashCode() 方法必须产生相同的整数结果。

为什么需要这个契约?

HashMapHashSet 的工作原理依赖于 hashCode()

  1. 计算对象的 hashCode() 来确定它应该被存储在哪个“桶”(bucket)中。
  2. 如果两个对象的 hashCode() 不同,它们会被放入不同的桶,系统会认为它们肯定不相等,不会调用 equals() 进行二次确认。
  3. 如果两个对象的 hashCode() 相同(发生了哈希碰撞),系统会认为它们可能相等,然后调用 equals() 方法来最终确认它们是否真的相等。

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

当你把对象放入 HashMapHashSet 时,会出现严重问题,两个内容相等(equals() 返回 true)的对象可能会有不同的 hashCode(),导致它们被存放在不同的位置,当你尝试通过一个对象去查找另一个内容相等的对象时,会因为 hashCode 不同而直接找不到,导致集合认为该对象不存在。

如何正确实现 hashCode()

同样推荐使用 Objects.hash() 方法,它会自动处理 null 值,并且基于你提供的字段生成一个哈希码。

// 前面的 Person 类中已经包含了
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

特殊情况:String包装类集合类

Java 的一些核心类已经为我们很好地重写了 equals()hashCode()

  • Stringequals() 比较的是字符串内容是否相同。
  • 包装类 (如 Integer, Double)equals() 比的是包装的基本值是否相同。
  • 集合类 (如 ArrayList, HashSet)equals() 比较的是集合的大小以及每个元素是否都相等(递归使用元素的 equals() 方法)。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1.equals(s2)); // true
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1.equals(i2)); // true
List<String> list1 = Arrays.asList("A", "B");
List<String> list2 = Arrays.asList("A", "B");
System.out.println(list1.equals(list2)); // true

总结与最佳实践

方法/概念 作用 默认行为 最佳实践
比较两个对象的引用地址是否相同。 比较引用地址。 用于判断是否为同一个对象实例。
equals() 比较两个对象的是否相同。 与 相同,比较引用地址。 必须重写以实现值相等判断,遵循 Object 类的 5 条约定。
hashCode() 返回一个对象的哈希码(整数)。 基于对象的内存地址生成一个哈希码。 在重写 equals() 后,必须重写 hashCode(),以满足它们之间的契约,使用 Objects.hash() 是最简单的方式。

清单:当你创建一个自定义类并需要比较其对象时

  1. 判断需求:你是否需要根据对象的“内容”来判断它们是否相等?
  2. 重写 equals():如果需要,请按照黄金法则重写 equals() 方法。
  3. 重写 hashCode():在重写 equals() 的同时,必须重写 hashCode()
  4. 重写 toString():强烈建议重写,以便于调试和日志输出。
  5. 考虑 Comparable 接口:如果你需要对对象进行排序(在 TreeSetArrays.sort() 中),你应该实现 Comparable 接口,并重写 compareTo() 方法。compareTo() 定义了对象的“自然排序”规则。
分享:
扫描分享到社交APP
上一篇
下一篇