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
}
}
注释:上面代码中,
str1和str2是两个独立创建的String对象,它们在内存中是两个不同的实例。==比较的是对象的引用地址,所以结果为false;而equals()方法在String类中被重写,比较的是字符串内容,因此返回true。
为什么需要重写 equals() 方法?
默认的 equals() 行为只适用于“引用相等”的场景。但在实际开发中,我们更关心的是“内容是否一致”。比如:
- 两个用户对象,如果用户名和年龄都一样,应该认为是“同一个用户”;
- 两个订单对象,如果订单号相同,就应该视为同一个订单;
- 两个图书对象,如果 ISBN 一致,就应视为同一本书。
这时候,我们就必须重写 equals() 方法,让它能根据业务逻辑判断“相等”。
重写规则:equals() 的契约(Contract)
Java 规定,equals() 方法必须满足以下四个条件,否则可能导致集合类行为异常:
- 自反性(Reflexive):
x.equals(x)必须返回true; - 对称性(Symmetric):如果
x.equals(y)为true,则y.equals(x)也必须为true; - 传递性(Transitive):如果
x.equals(y)为true,且y.equals(z)为true,则x.equals(z)也必须为true; - 一致性(Consistent):多次调用
equals()结果应一致,除非对象属性发生变化; - 对 null 的处理:
x.equals(null)应返回false。
提醒:违反这些规则,会导致
HashSet、HashMap等集合类出现不可预知的问题。
实战案例:自定义类中重写 equals() 方法
我们来创建一个 Person 类,并重写 equals() 方法,使其基于 name 和 age 字段判断相等。
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}]
}
}
注释:
p1和p2虽然是不同对象,但由于equals()返回true,HashSet认为它们是“同一个元素”,所以只保留一个。这正是我们想要的效果。
常见误区与避坑指南
误区一:只比较部分字段
// ❌ 错误写法:只比较 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() 方法,你不仅能写出更健壮的代码,还能在面试中脱颖而出。别再让“相等”变成一个模糊的概念,从今天起,让每一个比较都精准、可靠。