目录
equals()方法的定义与目的Object类中的equals()方法- 为什么需要重写
equals()方法? - 如何正确地重写
equals()方法?(黄金法则) - 重写
equals()的完整示例 equals()与 的区别(终极对比)- 重写
equals()时必须重写hashCode() - 总结与最佳实践
equals() 方法的定义与目的
equals() 方法是 java.lang.Object 类中的一个方法,它的核心目的是判断两个对象是否“相等”。

Object 类中提供的默认实现,对于判断“相等”的定义可能并不符合我们业务逻辑的需求,我们经常需要在自己的类中重写(Override)这个方法。
Object 类中的 equals() 方法
如果你不重写 equals() 方法,那么你的类就会继承 Object 类中的默认实现,我们来看一下它的源码:
public boolean equals(Object obj) {
return (this == obj);
}
这个默认实现非常简单:它只做了一件事,就是比较两个对象的内存地址是否相同。
this == obj:这里的 比较的是两个引用变量是否指向堆内存中的同一个对象实例(即内存地址是否相同)。- 返回值:如果指向同一个对象,返回
true;否则返回false。
示例:

public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public static void main(String[] args) {
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
Person p3 = p1;
System.out.println(p1.equals(p2)); // 输出 false,因为 p1 和 p2 是两个不同的对象实例
System.out.println(p1.equals(p3)); // 输出 true,因为 p3 和 p1 指向同一个对象实例
}
}
在这个例子中,p1 和 p2 虽然内容(name 属性)相同,但它们是 new 出来的两个独立对象,内存地址不同,p1.equals(p2) 返回 false,这通常不是我们想要的结果。
为什么需要重写 equals() 方法?
我们重写 equals() 方法,是为了改变“相等”的定义,我们希望根据对象“状态”(即对象的属性值)来判断它们是否相等,而不是根据它们的内存地址。
继续上面的 Person 类例子:
我们更希望认为两个 name 相同的 Person 对象是“相等”的,我们需要重写 equals() 方法。
如何正确地重写 equals() 方法?(黄金法则)
为了确保你的 equals() 方法行为正确、健壮,并且能被集合框架(如 HashMap, HashSet)正确使用,必须遵循以下黄金法则(通常指 Joshua Bloch 在《Effective Java》中提出的规范):

- 自反性:对于任何非
null的引用值x,x.equals(x)必须返回true。 - 对称性:对于任何非
null的引用值x和y,x.equals(y)返回true,y.equals(x)也必须返回true。 - 传递性:对于任何非
null的引用值x、y和z,x.equals(y)返回true,y.equals(z)也返回true,x.equals(z)也必须返回true。 - 一致性:对于任何非
null的引用值x和y,只要x和y所包含的信息没有被修改,多次调用x.equals(y)必须一致地返回true或false。 - 非空性:对于任何非
null的引用值x,x.equals(null)必须返回false。
推荐的 equals() 方法重写模板:
@Override
public boolean equals(Object o) {
// 1. 检查是否是同一个对象的引用(地址相同)
if (this == o) return true;
// 2. 检查参数是否为 null,或者是否属于不同的类
// 使用 getClass() 是更严格的检查,它要求类型完全一致
if (o == null || getClass() != o.getClass()) return false;
// 3. 将对象进行类型转换
MyClass that = (MyClass) o;
// 4. 比较类的“关键属性”
// 使用 Objects.equals() 可以安全地处理可能为 null 的属性
// 使用 == 比较基本类型
return Objects.equals(this.field1, that.field1) &&
this.field2 == that.field2;
// ... 继续比较其他重要属性
}
getClass() 和 instanceof 的选择:
getClass():这种方式更严格,它要求两个对象必须是同一个类的实例才可能相等。class B extends A,一个A的实例和一个B的实例即使内容完全一样,equals也会返回false。instanceof:这种方式更宽松,它允许一个类的实例和其子类的实例进行比较,如果使用instanceof,你需要确保子类不会破坏父类的相等性契约(这通常很复杂)。
通常推荐使用 getClass(),因为它更简单、更安全,也更符合“类型相同才能相等”的直觉。
重写 equals() 的完整示例
我们来重写 Person 类的 equals() 方法,使其根据 name 判断是否相等。
import java.util.Objects;
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 o) {
// 1. 自反性检查
if (this == o) return true;
// 2. 检查 null 和类型
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
Person person = (Person) o;
// 4. 比较关键属性
// 我们认为 name 是关键属性,age 不是
return Objects.equals(name, person.name);
}
// --- 为了完整性,也重写 toString() 方便打印 ---
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public static void main(String[] args) {
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 25); // age 不同
Person p3 = new Person("Bob", 30);
Person p4 = p1;
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true, 因为 name 都是 "Alice"
System.out.println("p1.equals(p3): " + p1.equals(p3)); // false, 因为 name 不同
System.out.println("p1.equals(p4): " + p1.equals(p4)); // true, 因为是同一个对象
System.out.println("p1.equals(null): " + p1.equals(null)); // false
}
}
输出:
p1.equals(p2): true
p1.equals(p3): false
p1.equals(p4): true
p1.equals(null): false
这个结果完全符合我们的预期。
equals() 与 的区别(终极对比)
这是 Java 面试中最经典的问题之一。
| 特性 | equals() |
|
|---|---|---|
| 基本类型 | 比较值,比较两个变量存储的数值是否相等。 | 不能使用。equals() 是方法,基本类型变量没有方法。 |
| 引用类型 | 比较引用(内存地址),判断两个变量是否指向堆中的同一个对象。 | 默认比较引用(与 相同),但可以被重写,用于比较对象的内容(状态)。 |
| 示例 | int a = 5; int b = 5; a == b; // truePerson p1 = new Person(); Person p2 = new Person(); p1 == p2; // false |
p1.equals(p2); // 默认为 false,取决于是否重写及重写逻辑 |
记忆口诀:
- 基本类型比值,引用类型比地址。
equals()默认比地址,但通常被重写来比内容。
重写 equals() 时必须重写 `hashCode()**
这是一个非常重要的约定,如果你违反了它,你的对象将无法在基于哈希的集合(如 HashMap, HashSet, Hashtable)中正常工作。
**
如果两个对象根据
equals()方法是相等的,那么调用这两个对象中任意一个对象的hashCode()方法都必须产生相同的整数结果。
为什么需要这个约定?
让我们看看 HashMap 的工作原理:
- 当你将一个键值对存入
HashMap时,HashMap会先计算键对象的hashCode()得到一个哈希值。 - 根据这个哈希值,找到
HashMap底层数组中的某个位置(桶)。 - 它会用
equals()方法去比较这个桶中的元素和你的新键是否相等。equals()返回true,说明是同一个键,会覆盖旧值。equals()返回false,说明发生了哈希冲突,会将新元素以链表或红黑树的形式挂在这个桶后面。
如果只重写 equals() 而不重写 hashCode() 会发生什么?
假设你定义了一个 Person 类,重写了 equals() 方法(根据 name 判断相等),但没有重写 hashCode(),那么它会使用 Object 类的默认 hashCode() 实现(基于内存地址)。
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
// p1.equals(p2) 返回 true
// p1.hashCode() 和 p2.hashCode() 几乎肯定不相等(因为它们是不同对象)
Map<Person, String> map = new HashMap<>();
map.put(p1, "First Alice");
// 现在尝试用 p2 作为 key 获取值
String value = map.get(p2); // 会返回 null!
原因分析:
map.put(p1, ...):计算p1.hashCode(),假设得到100,将(p1, "First Alice")存入哈希表100号桶。map.get(p2):计算p2.hashCode(),假设得到200,直接去哈希表200号桶查找,结果200号桶是空的,所以返回null,它根本没有机会去调用p2.equals(p1)!
为了保证基于哈希的集合能够正确工作,你必须遵守这个约定:“相等的对象必须有相同的哈希码”。
如何重写 hashCode()?
最简单、最推荐的方式是使用 Objects.hash() 方法:
@Override
public int hashCode() {
// 将你认为对“相等性”有贡献的属性组合起来
return Objects.hash(name);
}
Objects.hash() 内部会处理 null 值,并为多个属性生成一个合理的哈希码。
总结与最佳实践
- 理解默认行为:
Object的equals()比较内存地址。 - 明确需求:当你需要根据对象内容判断相等时,就必须重写
equals()。 - 遵循契约:重写
equals()时,必须严格遵守自反、对称、传递、一致和非空五大原则。 - 使用模板:推荐使用
getClass()+Objects.equals()的模板来安全地重写equals()。 - 黄金法则:永远、永远、永远在重写
equals()的同时重写hashCode(),并确保“相等的对象有相同的哈希码”。 - IDE 辅助:现代 IDE(如 IntelliJ IDEA, Eclipse)可以一键生成符合规范的
equals()和hashCode()方法,强烈推荐使用,而不是手动编写。
