Java Object equals() 方法(一文讲透)

Java Object equals() 方法:你真的懂它吗?

在 Java 开发中,判断两个对象是否“相等”看似简单,实则暗藏玄机。尤其是当你使用集合类(如 HashSet、HashMap)时,如果对 equals() 方法理解不深,很可能遇到“明明内容一样却无法去重”或“查不到数据”的问题。今天我们就来深入聊聊 Java 中的 equals() 方法,从基础原理到实战陷阱,帮你彻底掌握这个关键知识点。


equals() 方法的起源与默认行为

每个 Java 类都直接或间接继承自 java.lang.Object 类,而 Object 类中定义了 equals() 方法。它的默认实现非常简单:

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

这个实现的逻辑是:只有当两个引用指向同一个内存地址时,才认为它们相等。换句话说,它本质上是在比较“引用是否相同”,而不是“内容是否相同”。

这就像你和朋友都拿着一张相同的身份证,但如果你俩的身份证是同一张,那才能算“一样”;如果只是复印件,哪怕内容完全一样,系统也会认为是两张不同的证件。

public class EqualsDemo {
    public static void main(String[] args) {
        // 创建两个不同的对象,即使内容相同
        String str1 = new String("hello");
        String str2 = new String("hello");

        // 使用 equals() 比较内容
        System.out.println(str1.equals(str2)); // 输出:true

        // 使用 == 比较引用
        System.out.println(str1 == str2);      // 输出:false
    }
}

注释:上面代码中,str1str2 是两个独立创建的 String 对象,它们在内存中是两个不同的实例。== 比较的是对象的引用地址,所以结果为 false;而 equals() 方法在 String 类中被重写,比较的是字符串内容,因此返回 true


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

默认的 equals() 行为只适用于“引用相等”的场景。但在实际开发中,我们更关心的是“内容是否一致”。比如:

  • 两个用户对象,如果用户名和年龄都一样,应该认为是“同一个用户”;
  • 两个订单对象,如果订单号相同,就应该视为同一个订单;
  • 两个图书对象,如果 ISBN 一致,就应视为同一本书。

这时候,我们就必须重写 equals() 方法,让它能根据业务逻辑判断“相等”。

重写规则:equals() 的契约(Contract)

Java 规定,equals() 方法必须满足以下四个条件,否则可能导致集合类行为异常:

  1. 自反性(Reflexive)x.equals(x) 必须返回 true
  2. 对称性(Symmetric):如果 x.equals(y)true,则 y.equals(x) 也必须为 true
  3. 传递性(Transitive):如果 x.equals(y)true,且 y.equals(z)true,则 x.equals(z) 也必须为 true
  4. 一致性(Consistent):多次调用 equals() 结果应一致,除非对象属性发生变化;
  5. 对 null 的处理x.equals(null) 应返回 false

提醒:违反这些规则,会导致 HashSetHashMap 等集合类出现不可预知的问题。


实战案例:自定义类中重写 equals() 方法

我们来创建一个 Person 类,并重写 equals() 方法,使其基于 nameage 字段判断相等。

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 obj) {
        // 1. 如果是同一个对象,直接返回 true
        if (this == obj) {
            return true;
        }

        // 2. 如果传入的是 null,返回 false
        if (obj == null) {
            return false;
        }

        // 3. 如果类型不一致,返回 false
        if (getClass() != obj.getClass()) {
            return false;
        }

        // 4. 强制类型转换为 Person 类型
        Person other = (Person) obj;

        // 5. 比较关键字段:name 和 age
        // 注意:字符串比较要用 equals(),不能用 ==
        return age == other.age && (name == null ? other.name == null : name.equals(other.name));
    }

    // 重写 hashCode() 方法(非常重要!)
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    // 为了方便查看,重写 toString()
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

注释

  • 第一步判断 this == obj 是性能优化,避免不必要的比较;
  • obj == null 的判断防止空指针异常;
  • getClass() != obj.getClass() 确保类型一致,防止子类对象误判;
  • name == null ? other.name == null : name.equals(other.name) 是安全的字符串比较写法;
  • 重写 hashCode() 是关键!如果 equals() 改了,hashCode() 也必须同步改,否则集合类会出问题。

集合类中的 equals() 行为验证

我们用 HashSet 来验证重写后的 equals() 是否生效。

import java.util.HashSet;

public class HashSetTest {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();

        Person p1 = new Person("张三", 25);
        Person p2 = new Person("张三", 25);
        Person p3 = new Person("李四", 30);

        set.add(p1);
        set.add(p2); // 由于 equals() 判断相同,不会重复添加
        set.add(p3);

        System.out.println("集合大小:" + set.size()); // 输出:2
        System.out.println("集合内容:" + set);
        // 输出:[Person{name='张三', age=25}, Person{name='李四', age=30}]
    }
}

注释p1p2 虽然是不同对象,但由于 equals() 返回 trueHashSet 认为它们是“同一个元素”,所以只保留一个。这正是我们想要的效果。


常见误区与避坑指南

误区一:只比较部分字段

// ❌ 错误写法:只比较 age,忽略 name
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Person other = (Person) obj;
    return age == other.age; // 忽略 name,会导致逻辑错误
}

后果:两个不同的人,只要年龄一样就会被当作“相等”,这显然不符合业务逻辑。

误区二:忘记重写 hashCode()

// ❌ 错误:只重写了 equals(),没重写 hashCode()
@Override
public boolean equals(Object obj) {
    // ... 比较逻辑
    return age == other.age && name.equals(other.name);
}

后果:虽然 equals() 比较正确,但 hashCode() 未同步修改,会导致 HashMap 中 key 无法正确查找,出现“明明有数据却查不到”的问题。

误区三:使用 == 比较字符串

// ❌ 错误写法
return age == other.age && name == other.name;

后果name == other.name 只比较引用,可能返回 false,即使字符串内容相同。


高级技巧:使用工具类简化 equals() 编写

为了避免手写 equals() 时出错,推荐使用 Lombok 或 Apache Commons Lang 提供的工具。

使用 Lombok(推荐)

import lombok.EqualsAndHashCode;

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

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 无需手动写 equals() 和 hashCode()
}

优点:自动生成符合规范的 equals()hashCode(),减少出错概率。


总结:Java Object equals() 方法的核心要点

  • equals() 方法默认比较引用,不是内容;
  • 业务对象应根据实际需求重写 equals()
  • 重写时必须遵守“契约”:自反、对称、传递、一致、非 null;
  • 重写 equals() 时,必须同步重写 hashCode()
  • 集合类(如 HashSet、HashMap)依赖 equals()hashCode() 正常工作;
  • 使用工具类(如 Lombok)可减少手写错误。

最后提醒一句:在 Java 中,equals() 方法不只是“比较相等”,更是你对“对象本质”的定义。每一次重写,都是一次对业务逻辑的确认。

掌握好 Java Object equals() 方法,你不仅能写出更健壮的代码,还能在面试中脱颖而出。别再让“相等”变成一个模糊的概念,从今天起,让每一个比较都精准、可靠。