为什么需要重写 equals 方法?
Java 中 Object 类提供了 equals 方法的默认实现:

public boolean equals(Object obj) {
return (this == obj);
}
这个实现比较的是两个对象的内存地址(即引用),只有当两个对象是同一个实例时(this 和 obj 指向内存中的同一块地址),它才返回 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是同一个对象
}
}
在上面的例子中,p1 和 p2 的内容(name 和 age)完全相同,但从内存角度看,它们是两个独立的对象,默认的 equals 方法会返回 false,这显然不符合我们的业务逻辑,我们需要重写 equals 方法,使其比较 name 和 age 是否相等。

如何正确地重写 equals 方法?
根据 Joshua Bloch 在《Effective Java》中的建议,一个健壮的 equals 方法应该遵循以下步骤:
- 使用 检查引用是否相等:
this和other是同一个对象,直接返回true,这是一个快速优化。 - 检查
other是否为null:other是null,则返回false。 - 检查类型:使用
instanceof检查other是否是当前类的实例,如果不是,返回false,这一步是为了保证类型安全。 - 进行类型转换:将
other转换为当前类的类型,以便访问其成员变量。 - 比较关键字段:比较当前对象的关键字段是否与转换后对象的对应字段相等,建议使用
Objects.equals()方法来比较字段,因为它可以安全地处理null值。 - 返回结果:如果所有关键字段都相等,则返回
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()方法比较是相等的,那么调用这两个对象的hashCode()方法必须产生相同的整数结果。
为什么?
因为 hashCode() 方法的主要作用是配合哈希集合(如 HashMap, HashSet, Hashtable)使用的,这些集合通过哈希码来确定对象在哈希表中的“桶”位置。
- 当你向
HashSet中添加一个对象时,HashSet会先计算该对象的hashCode(),找到对应的桶。 - 然后它会检查这个桶中是否已经有其他对象。
- 如果桶为空,直接放入。
- 如果桶不为空,它会使用
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!
原因分析:
p1被添加到HashSet。HashSet调用p1.hashCode()得到一个值,假设为123,并将其放入123号桶。- 当你调用
personSet.contains(p2)时,HashSet会计算p2.hashCode(),由于你没有重写hashCode,它使用Object类的hashCode(),这个方法是基于内存地址的。p2和p1是两个不同的对象,所以它们的内存地址不同,p2.hashCode()可能会得到另一个值,456。 HashSet会去456号桶里查找,当然找不到p1,所以返回false。
这导致了严重的问题:两个逻辑上相等的对象,在 HashSet 中却被认为是两个不同的对象,破坏了集合的语义。
解决方案:
如上面的 Person 类所示,使用 Objects.hash() 方法可以轻松生成一个基于所有关键字段的哈希码。
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Objects.hash() 内部会处理 null 值,并为每个字段生成一个哈希码,然后将它们组合起来,确保如果 equals 相等,hashCode 也必然相等。
最佳实践和注意事项
- 总是同时重写
equals和hashCode:这是铁律。 - 使用
@Override注解:这可以防止你因为方法签名写错(比如参数类型错误)而导致的意外重写失败。 - 确定哪些是“关键字段”:只有那些参与对象相等性判断的字段才需要比较和计算哈希码,数据库主键、ID等通常是关键字段。
instanceof与getClass()的选择:instanceof:如果你希望一个类的实例可以和它的任何子类的实例进行比较(前提是子类没有增加新的、影响相等性的字段),使用instanceof,这是更常见的做法。getClass():如果你严格要求类型必须完全一致(即String不能和StringBuilder比较,即使它们内容相同),使用getClass() != o.getClass()。
- 不要比较“可变”字段:如果对象包含可变字段,而这些字段又参与了
equals的比较,那么对象在放入集合后,其状态被改变,可能会导致集合中的对象“丢失”或无法找到,通常建议将参与equals比较的字段设为final。 - 性能考虑:将成本最低、最有可能不相等的字段放在
equals比较的最前面,这样可以快速返回false,避免不必要的后续比较。
