- 引用相等 (Reference Equality):判断两个引用变量是否指向内存中的同一个对象。
- 值相等 (Value Equality):判断两个对象的内容(状态)是否相同,即使它们是内存中不同的两个对象。
下面我们来详细讲解这两种方式,以及如何正确地实现自定义对象的值相等判断。

引用相等 ( 运算符)
这是最基本、最快速的判断方式。 比较的是两个变量在内存中的地址(引用)。
- 规则:如果两个引用指向同一个对象实例(内存地址相同),则 返回
true;否则返回false。 - 适用场景:通常用于判断是否为同一个对象实例。
- 示例:
public class ReferenceEqualityExample {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = s1;
// s1 和 s2 是两个不同的对象,引用地址不同
System.out.println("s1 == s2: " + (s1 == s2)); // 输出: false
// s1 和 s3 指向同一个对象,引用地址相同
System.out.println("s1 == s3: " + (s1 == s3)); // 输出: true
}
}
值相等 (equals() 方法)
当我们需要比较两个对象的内容是否相同时,应该使用 equals() 方法。equals() 是 Object 类中的一个方法,所有 Java 类都继承自 Object。
1 Object 类中的 equals()
Object 类中默认的 equals() 方法实现与 运算符完全相同,它也是比较两个对象的引用地址。
public class Object {
// ...
public boolean equals(Object obj) {
return (this == obj);
}
// ...
}
这意味着,如果你直接使用一个自定义类的实例来调用 equals(),它默认的行为仍然是判断引用是否相等。

2 重写 equals() 方法
为了实现“值相等”,我们必须在自己的类中重写 (Override) equals() 方法。
重写 equals() 的黄金法则(来自《Effective Java》):
- 自反性:对于任何非
null的引用x,x.equals(x)必须返回true。 - 对称性:对于任何非
null的引用x和y,x.equals(y)必须与y.equals(x)具有相同的返回值。 - 传递性:对于任何非
null的引用x、y和z,x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也必须返回true。 - 一致性:对于任何非
null的引用x和y,只要对象中用于比较的信息没有被修改,多次调用x.equals(y)必须 consistently 返回相同的值。 - 非空性:对于任何非
null的引用x,x.equals(null)必须返回false。
3 正确重写 equals() 的步骤和模板
一个健壮的 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 myClass = (MyClass) o;
// 4. 比较类的“关键”字段
// 使用 Objects.equals() 可以安全地处理可能为 null 的字段
// 比较基本类型时,使用 == 即可
return Objects.equals(this.field1, myClass.field1) &&
this.field2 == myClass.field2 &&
Objects.equals(this.field3, myClass.field3);
}
示例:重写 Person 类的 equals() 方法
假设我们有一个 Person 类,我们希望两个 Person 对象的 name 和 age 相同就认为它们相等。
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;
}
// Getters and Setters
public String getName() { return name; }
public int getAge() { return 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. 比较关键字段
// age 是基本类型,用 ==
// name 是引用类型,用 Objects.equals() 来处理 null 的情况
return age == person.age &&
Objects.equals(name, person.name);
}
// 重要:重写 equals() 后,强烈建议也重写 hashCode()!
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// 为了方便调试,也建议重写 toString()
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试 equals() 方法
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", 30);
Person p4 = p1;
System.out.println("p1.equals(p2): " + p1.equals(p2)); // 输出: true (内容相同)
System.out.println("p1 == p2: " + (p1 == p2)); // 输出: false (不是同一个对象)
System.out.println("p1.equals(p3): " + p1.equals(p3)); // 输出: false (内容不同)
System.out.println("p1.equals(p4): " + p1.equals(p4)); // 输出: true (同一个对象)
}
}
equals() 与 hashCode() 的契约关系
这是一个非常重要的概念,尤其当你将对象用作 HashMap 的键或 HashSet 的元素时。
契约:如果两个对象根据 equals() 方法判断是相等的,那么调用它们的 hashCode() 方法必须产生相同的整数结果。
为什么需要这个契约?
HashMap 和 HashSet 的工作原理依赖于 hashCode():
- 计算对象的
hashCode()来确定它应该被存储在哪个“桶”(bucket)中。 - 如果两个对象的
hashCode()不同,它们会被放入不同的桶,系统会认为它们肯定不相等,不会调用equals()进行二次确认。 - 如果两个对象的
hashCode()相同(发生了哈希碰撞),系统会认为它们可能相等,然后调用equals()方法来最终确认它们是否真的相等。
如果只重写 equals() 而不重写 hashCode() 会发生什么?
当你把对象放入 HashMap 或 HashSet 时,会出现严重问题,两个内容相等(equals() 返回 true)的对象可能会有不同的 hashCode(),导致它们被存放在不同的位置,当你尝试通过一个对象去查找另一个内容相等的对象时,会因为 hashCode 不同而直接找不到,导致集合认为该对象不存在。
如何正确实现 hashCode()?
同样推荐使用 Objects.hash() 方法,它会自动处理 null 值,并且基于你提供的字段生成一个哈希码。
// 前面的 Person 类中已经包含了
@Override
public int hashCode() {
return Objects.hash(name, age);
}
特殊情况:String、包装类和集合类
Java 的一些核心类已经为我们很好地重写了 equals() 和 hashCode()。
String:equals()比较的是字符串内容是否相同。- 包装类 (如
Integer,Double):equals()比的是包装的基本值是否相同。 - 集合类 (如
ArrayList,HashSet):equals()比较的是集合的大小以及每个元素是否都相等(递归使用元素的equals()方法)。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1.equals(s2)); // true
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1.equals(i2)); // true
List<String> list1 = Arrays.asList("A", "B");
List<String> list2 = Arrays.asList("A", "B");
System.out.println(list1.equals(list2)); // true
总结与最佳实践
| 方法/概念 | 作用 | 默认行为 | 最佳实践 |
|---|---|---|---|
| 比较两个对象的引用地址是否相同。 | 比较引用地址。 | 用于判断是否为同一个对象实例。 | |
equals() |
比较两个对象的是否相同。 | 与 相同,比较引用地址。 | 必须重写以实现值相等判断,遵循 Object 类的 5 条约定。 |
hashCode() |
返回一个对象的哈希码(整数)。 | 基于对象的内存地址生成一个哈希码。 | 在重写 equals() 后,必须重写 hashCode(),以满足它们之间的契约,使用 Objects.hash() 是最简单的方式。 |
清单:当你创建一个自定义类并需要比较其对象时
- 判断需求:你是否需要根据对象的“内容”来判断它们是否相等?
- 重写
equals():如果需要,请按照黄金法则重写equals()方法。 - 重写
hashCode():在重写equals()的同时,必须重写hashCode()。 - 重写
toString():强烈建议重写,以便于调试和日志输出。 - 考虑
Comparable接口:如果你需要对对象进行排序(在TreeSet或Arrays.sort()中),你应该实现Comparable接口,并重写compareTo()方法。compareTo()定义了对象的“自然排序”规则。
