这是一个在 Java 编程中至关重要且容易出错的话题,理解并正确地重写 equals() 方法对于编写健壮、可预测的代码至关重要。

为什么需要重写 equals()?
我们要明白 equals() 的默认行为是什么。
在 Java 中,所有类都直接或间接继承自 Object 类。Object 类中提供的 equals() 方法的默认实现是:
public boolean equals(Object obj) {
return (this == obj);
}
这个实现比较的是两个对象的内存地址(即引用),只有当两个引用指向同一个对象时(this 和 obj 是同一个东西),它才返回 true。
问题场景:
考虑一个 Person 类,我们希望两个 Person 对象如果拥有相同的 name 和 age,就认为它们是“相等”的,即使它们是两个不同的实例(内存地址不同)。

Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25);
// 使用 Object 默认的 equals()
System.out.println(p1.equals(p2)); // 输出 false,因为 p1 和 p2 是两个不同的对象
显然,这不符合我们的业务逻辑,我们需要重写 equals() 方法,来定义我们自己的“相等”标准。
重写 equals() 的正确方法(黄金法则)
要正确地重写 equals() 方法,你必须遵循 Java 官方文档(来自 Object 类的 equals 方法的契约)中的所有约定,否则,你的类在集合(如 HashSet, HashMap)中可能无法正常工作,或者与其他依赖 equals() 方法的 API 产生不可预测的行为。
以下是必须遵循的五大黄金法则:
- 自反性:对于任何非
null的引用值x,x.equals(x)必须返回true。 - 对称性:对于任何非
null的引用值x和y,x.equals(y)必须与y.equals(x)具有相同的返回值。 - 传递性:对于任何非
null的引用值x、y和z,x.equals(y)返回truey.equals(z)返回true,x.equals(z)也必须返回true。 - 一致性:对于任何非
null的引用值x和y,只要对象上用于equals比较的信息没有被修改,多次调用x.equals(y)必须一致地返回true或false。 - 非空性:对于任何非
null的引用值x,x.equals(null)必须返回false。
手动重写 equals() 的标准步骤
遵循上述原则,我们可以总结出一个安全、可靠的重写步骤,我们以一个 Student 类为例:

public class Student {
private String studentId;
private String name;
private int age;
// 构造函数、getters 和 setters...
public Student(String studentId, String name, int age) {
this.studentId = studentId;
this.name = name;
this.age = age;
}
// ... getters and setters
}
检查“是否为同一个对象”
使用 操作符检查比较的引用是否是同一个对象,如果是,直接返回 true,这可以快速判断,提高性能,也满足了自反性。
@Override
public boolean equals(Object o) {
// 1. 检查是否为同一个对象
if (this == o) {
return true;
}
检查“参数是否为 null 或类型是否匹配”
如果参数 o 是 null,或者 o 的类型不是 Student,那么它们肯定不相等,使用 instanceof 操作符进行类型检查,这满足了非空性。
// 2. 检查参数是否为 null 或类型是否匹配
if (o == null || getClass() != o.getClass()) {
return false;
}
getClass() != o.getClass()是比instanceof更严格的检查,它确保了子类不会与父类“相等”,除非你明确希望这样,对于大多数情况,这是更好的选择。
进行类型转换
既然通过了类型检查,我们就可以将 Object 类型的参数 o 安全地转换为 Student 类型。
// 3. 将对象进行类型转换
Student student = (Student) o;
比较“关键字段”
比较决定对象“相等性”的关键字段,对于基本类型,使用 ;对于对象类型,使用 Objects.equals() 或 String.equals()(避免 NullPointerException),我们比较所有字段,但也可以根据业务逻辑选择性地比较。
// 4. 比较关键字段
// 使用 Objects.equals 可以安全地处理可能为 null 的字段
return Objects.equals(studentId, student.studentId) &&
Objects.equals(name, student.name) &&
age == student.age;
}
- 为什么使用
Objects.equals()? 因为它可以安全地处理null值,避免了NullPointerException。Objects.equals(a, b)内部实现是a == b || (a != null && a.equals(b))。
完整的 equals() 方法示例
将以上步骤整合起来,我们得到一个完整且健壮的 equals() 方法:
import java.util.Objects;
public class Student {
private String studentId;
private String name;
private int age;
// 构造函数、getters 和 setters...
public Student(String studentId, String name, int age) {
this.studentId = studentId;
this.name = name;
this.age = age;
}
// ... getters and setters
/**
* 重写 equals 方法
*/
@Override
public boolean equals(Object o) {
// 1. 检查是否为同一个对象
if (this == o) return true;
// 2. 检查参数是否为 null 或类型是否匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 将对象进行类型转换
Student student = (Student) o;
// 4. 比较关键字段
return age == student.age &&
Objects.equals(studentId, student.studentId) &&
Objects.equals(name, student.name);
}
// ... hashCode() 方法(下面会讲)
}
必须同时重写 hashCode() 方法
这是最容易被忽略,但又至关重要的一点。
hashCode() 约定:
- 在 Java 应用程序的一次执行期间,只要对象的
equals方法所用的信息没有被修改,那么对同一个对象多次调用hashCode方法,必须一致地返回同一个整数。 - 如果两个对象根据
equals(Object)方法是相等的,那么调用这两个对象的hashCode方法必须产生相同的整数结果。 - 如果两个对象根据
equals(Object)方法是不相等的,那么对这两个对象调用hashCode方法,不要求一定产生不同的整数结果。(程序员应该知道,为不相等的对象生成不同的hashCode可以提高哈希表的性能。)
为什么必须同时重写?
因为 Java 中许多基于哈希的集合类(如 HashSet, HashMap, Hashtable)都依赖这两个方法。
工作原理:
- 当你向
HashSet中添加一个对象时,HashSet首先会调用该对象的hashCode()方法,计算出其哈希码,并确定一个“桶”(bucket)的位置。 - 如果该“桶”为空,就直接将对象存入。
- 如果该“桶”不为空,
HashSet会遍历该“桶”中的所有对象,并使用equals()方法与新对象进行比较。equals()返回true,说明对象已存在,HashSet不会重复添加。equals()返回false,说明这是一个“哈希冲突”(Hash Collision),新对象会被作为链表或红黑树节点添加到这个“桶”中。
错误示范:
如果你只重写了 equals() 而没有重写 hashCode(),会发生什么?
Student s1 = new Student("101", "Alice", 20);
Student s2 = new Student("101", "Alice", 20);
HashSet<Student> set = new HashSet<>();
set.add(s1);
System.out.println("集合中是否包含 s2? " + set.contains(s2)); // 可能输出 false!
s1和s2通过equals()比算是相等的。s1和s2是两个不同的对象,它们的默认hashCode()值(基于内存地址)不同。- 当
s1被加入HashSet时,它被存入根据其hashCode计算出的“桶” A。 - 当
set.contains(s2)被调用时,HashSet计算s2的hashCode,发现它指向了不同的“桶” B,于是直接返回false,甚至不会去调用equals()方法进行比较。
如果两个对象相等,它们的 hashCode 必须相等,一旦你重写了 equals(),就必须同时重写 hashCode()。
如何重写 hashCode()?
最简单、最推荐的方式是使用 IDE(如 IntelliJ IDEA 或 Eclipse)自动生成,它们生成的代码非常标准和高效。
手动重写时,可以使用 Objects.hash() 方法,它会帮你处理所有细节:
@Override
public int hashCode() {
return Objects.hash(studentId, name, age);
}
这个方法内部会为每个传入的参数计算哈希码,并将它们组合起来,完全符合 hashCode() 的约定。
最佳实践与总结
- 何时重写? 当你的类需要基于业务逻辑判断对象是否“相等”,并且该类会被用作
HashMap、HashSet或Hashtable的键,或者被放入这些集合中时,必须重写equals()和hashCode()。 - 不要重写:如果类的“相等性”永远基于内存地址(即
this == obj),或者类是不可变的,并且你确定它永远不会被用于哈希集合,那么你可以不重写(使用Object的默认实现)。 - 使用 IDE:强烈建议使用 IDE 的自动生成功能(IntelliJ 中是
Code -> Generate -> equals() and hashCode()),这可以避免手写错误,并保持代码的一致性。 - 字段一致性:
equals()和hashCode()中比较的字段必须完全一致,如果在equals()中比较了name和age,那么在hashCode()中也必须使用name和age来计算哈希码。 - 性能:将最有可能不相同的、计算成本低的字段放在
equals()比较的前面,可以更快地返回false,提高性能。
通过遵循这些规则,你可以确保你的类行为符合 Java 的约定,在各种场景下都能正常工作。
