杰瑞科技汇

Java重写equals方法需注意哪些关键点?

为什么需要重写 equals 方法?

Java 中 Object 类提供了 equals 方法的默认实现:

Java重写equals方法需注意哪些关键点?-图1
(图片来源网络,侵删)
public boolean equals(Object obj) {
    return (this == obj);
}

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

在很多情况下,这并不是我们想要的行为,我们通常希望比较的是对象的内容(或状态)是否相等,而不是它们的地址。

举个例子:

public class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        Person p3 = p1;
        System.out.println(p1 == p2); // false, 因为是两个不同的对象实例
        System.out.println(p1.equals(p2)); // 默认也是 false,因为equals默认比较内存地址
        System.out.println(p1 == p3); // true, 因为p3和p1是同一个对象
    }
}

在上面的例子中,p1p2 的内容(nameage)完全相同,但从内存角度看,它们是两个独立的对象,默认的 equals 方法会返回 false,这显然不符合我们的业务逻辑,我们需要重写 equals 方法,使其比较 nameage 是否相等。

Java重写equals方法需注意哪些关键点?-图2
(图片来源网络,侵删)

如何正确地重写 equals 方法?

根据 Joshua Bloch 在《Effective Java》中的建议,一个健壮的 equals 方法应该遵循以下步骤:

  1. 使用 检查引用是否相等thisother 是同一个对象,直接返回 true,这是一个快速优化。
  2. 检查 other 是否为 nullothernull,则返回 false
  3. 检查类型:使用 instanceof 检查 other 是否是当前类的实例,如果不是,返回 false,这一步是为了保证类型安全。
  4. 进行类型转换:将 other 转换为当前类的类型,以便访问其成员变量。
  5. 比较关键字段:比较当前对象的关键字段是否与转换后对象的对应字段相等,建议使用 Objects.equals() 方法来比较字段,因为它可以安全地处理 null 值。
  6. 返回结果:如果所有关键字段都相等,则返回 true,否则返回 false

重写 equals 方法的代码示例

下面我们以 Person 类为例,实现一个完整的 equals 方法。

import java.util.Objects;
public class Person {
    private final String name; // 使用final,意味着一旦赋值就不能改变
    private final int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // Getter方法
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    /**
     * 重写equals方法
     */
    @Override
    public boolean equals(Object o) {
        // 1. 检查引用是否相等(同一对象)
        if (this == o) {
            return true;
        }
        // 2. 检查o是否为null
        if (o == null) {
            return false;
        }
        // 3. 检查类型是否匹配
        // 注意:如果允许子类比较,应该使用 getClass() != o.getClass()
        // 如果希望子类可以和父类比较(当子类没有额外字段时),可以使用 instanceof
        // 这里我们使用 instanceof,因为它更灵活
        if (!(o instanceof Person)) {
            return false;
        }
        // 4. 类型转换
        Person person = (Person) o;
        // 5. 比较关键字段
        // 使用Objects.equals来安全比较String,避免NullPointerException
        return age == person.age &&
                Objects.equals(name, person.name);
    }
    // 重写hashCode方法(非常重要!见下文)
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    // 可选:重写toString方法,方便打印和调试
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试代码:

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", 25);
        Person p4 = null;
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
        System.out.println("p1.equals(p4): " + p1.equals(p4)); // false
        System.out.println("p1.equals(null): " + p1.equals(null)); // false
        System.out.println("p1.equals(\"hello\"): " + p1.equals("hello")); // false
    }
}

必须同时重写 hashCode 方法

这是一个极其重要的规则,几乎和重写 equals 一样重要。

Java重写equals方法需注意哪些关键点?-图3
(图片来源网络,侵删)

Java 规范规定:

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

为什么? 因为 hashCode() 方法的主要作用是配合哈希集合(如 HashMap, HashSet, Hashtable)使用的,这些集合通过哈希码来确定对象在哈希表中的“桶”位置。

  1. 当你向 HashSet 中添加一个对象时,HashSet 会先计算该对象的 hashCode(),找到对应的桶。
  2. 然后它会检查这个桶中是否已经有其他对象。
  3. 如果桶为空,直接放入。
  4. 如果桶不为空,它会使用 equals() 方法去比较新对象和桶中已有的对象是否相等。
    • equals() 返回 true,说明是重复元素,HashSet 不会添加。
    • equals() 返回 false,说明是不同的对象,但哈希冲突了,HashSet 会将新对象以链表或红黑树的形式存入同一个桶中。

如果你只重写了 equals 而没有重写 hashCode 会发生什么?

// 假设Person类只重写了equals,没有重写hashCode
Set<Person> personSet = new HashSet<>();
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
personSet.add(p1);
System.out.println("Set contains p2? " + personSet.contains(p2)); // 可能会打印 false!

原因分析:

  1. p1 被添加到 HashSetHashSet 调用 p1.hashCode() 得到一个值,假设为 123,并将其放入 123 号桶。
  2. 当你调用 personSet.contains(p2) 时,HashSet 会计算 p2.hashCode(),由于你没有重写 hashCode,它使用 Object 类的 hashCode(),这个方法是基于内存地址的。p2p1 是两个不同的对象,所以它们的内存地址不同,p2.hashCode() 可能会得到另一个值,456
  3. HashSet 会去 456 号桶里查找,当然找不到 p1,所以返回 false

这导致了严重的问题:两个逻辑上相等的对象,在 HashSet 中却被认为是两个不同的对象,破坏了集合的语义。

解决方案: 如上面的 Person 类所示,使用 Objects.hash() 方法可以轻松生成一个基于所有关键字段的哈希码。

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

Objects.hash() 内部会处理 null 值,并为每个字段生成一个哈希码,然后将它们组合起来,确保如果 equals 相等,hashCode 也必然相等。


最佳实践和注意事项

  1. 总是同时重写 equalshashCode:这是铁律。
  2. 使用 @Override 注解:这可以防止你因为方法签名写错(比如参数类型错误)而导致的意外重写失败。
  3. 确定哪些是“关键字段”:只有那些参与对象相等性判断的字段才需要比较和计算哈希码,数据库主键、ID等通常是关键字段。
  4. instanceofgetClass() 的选择
    • instanceof:如果你希望一个类的实例可以和它的任何子类的实例进行比较(前提是子类没有增加新的、影响相等性的字段),使用 instanceof,这是更常见的做法。
    • getClass():如果你严格要求类型必须完全一致(即 String 不能和 StringBuilder 比较,即使它们内容相同),使用 getClass() != o.getClass()
  5. 不要比较“可变”字段:如果对象包含可变字段,而这些字段又参与了 equals 的比较,那么对象在放入集合后,其状态被改变,可能会导致集合中的对象“丢失”或无法找到,通常建议将参与 equals 比较的字段设为 final
  6. 性能考虑:将成本最低、最有可能不相等的字段放在 equals 比较的最前面,这样可以快速返回 false,避免不必要的后续比较。
分享:
扫描分享到社交APP
上一篇
下一篇