1. 概述
在本篇文章中,我们将深入探讨两个紧密相关的 Java 方法:equals()
和 hashCode()
。重点在于它们之间的契约关系、如何正确地重写这两个方法,以及为什么我们要么同时重写,要么都不重写。
2. equals()
方法
默认情况下,Java 中的所有类都继承自 Object
类,因此每个类都隐式地拥有 equals()
和 hashCode()
方法:
class Money {
int amount;
String currencyCode;
}
Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");
boolean balanced = income.equals(expenses);
我们可能会期望 income.equals(expenses)
返回 true
,但以当前的实现来看,它并不会。
✅ 原因: Object
类中的 equals()
方法默认比较的是对象的引用(identity),而不是内容(value)。在这个例子中,income
和 expenses
是两个不同的对象实例,所以 equals()
返回 false
。
要改变这种行为,我们必须重写 equals()
方法。
2.1. 重写 equals()
让我们重写 equals()
方法,让它不仅比较对象身份,还要比较关键属性的值:
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Money))
return false;
Money other = (Money)o;
boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
return this.amount == other.amount && currencyCodeEquals;
}
✅ 上面的实现中,我们进行了三步判断:
- 如果是同一个对象,直接返回
true
- 如果不是
Money
类型的对象,返回false
- 比较两个
Money
对象的amount
和currencyCode
属性是否相等
2.2. equals()
的契约
Java SE 定义了 equals()
方法必须遵守的契约。虽然这些规则看起来符合常识,但我们仍需明确以下几点:
✅ equals()
必须满足的条件:
- ✅ 自反性(reflexive):对象必须等于自己
- ✅ 对称性(symmetric):
x.equals(y)
的结果必须和y.equals(x)
相同 - ✅ 传递性(transitive):若
x.equals(y)
且y.equals(z)
,则x.equals(z)
也必须成立 - ✅ 一致性(consistent):只要参与比较的属性没有改变,
equals()
的结果就不能变
详细契约内容可查阅 Java SE 文档。
2.3. 继承破坏 equals()
的对称性
尽管契约看起来很合理,但在使用继承时很容易违反这些规则。例如,我们定义一个继承自 Money
的 WrongVoucher
类:
class WrongVoucher extends Money {
private String store;
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof WrongVoucher))
return false;
WrongVoucher other = (WrongVoucher)o;
boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return this.amount == other.amount && currencyCodeEquals && storeEquals;
}
// 其他方法...
}
现在来看这段代码的运行结果:
Money cash = new Money(42, "USD");
WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon");
voucher.equals(cash) => false // 正常
cash.equals(voucher) => true // ❌ 错误!违反对称性
❌ 问题: 这违反了 equals()
的对称性契约。
2.4. 使用组合修复对称性问题
✅ 推荐做法: 避免通过继承来扩展类,优先使用组合。
我们将 Voucher
改为包含 Money
属性的类:
class Voucher {
private Money value;
private String store;
Voucher(int amount, String currencyCode, String store) {
this.value = new Money(amount, currencyCode);
this.store = store;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Voucher))
return false;
Voucher other = (Voucher) o;
boolean valueEquals = (this.value == null && other.value == null)
|| (this.value != null && this.value.equals(other.value));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return valueEquals && storeEquals;
}
// 其他方法...
}
✅ 这样修改后,equals()
方法的行为就符合契约了。
3. hashCode()
方法
hashCode()
方法返回一个整数,代表当前对象的“哈希码”。这个值应该与 equals()
的定义保持一致。
3.1. hashCode()
的契约
Java SE 同样为 hashCode()
定义了契约,且与 equals()
密切相关:
✅ hashCode()
必须满足的条件:
- ✅ 内部一致性(internal consistency):只要参与
equals()
判断的属性未变,hashCode()
的值就不能变 - ✅ 相等一致性(equals consistency):如果两个对象通过
equals()
比较相等,那么它们的hashCode()
必须相同 - ✅ 冲突允许(collisions allowed):不相等的对象可以有相同的
hashCode()
(哈希冲突)
3.2. 破坏 hashCode()
与 equals()
一致性
最常见的问题之一是:**只重写了 equals()
而没有重写 hashCode()
**。
来看一个例子:
class Team {
String city;
String department;
@Override
public final boolean equals(Object o) {
// 实现省略
}
}
❌ 问题: 虽然 Team
重写了 equals()
,但使用的是默认的 hashCode()
实现。这意味着即使两个对象通过 equals()
判断为相等,它们也可能返回不同的哈希码。
3.3. HashMap 中的哈希码不一致问题
这在使用哈希集合(如 HashMap
)时会导致严重问题。例如:
Map<Team,String> leaders = new HashMap<>();
leaders.put(new Team("New York", "development"), "Anne");
leaders.put(new Team("Boston", "development"), "Brian");
leaders.put(new Team("Boston", "marketing"), "Charlie");
Team myTeam = new Team("New York", "development");
String myTeamLeader = leaders.get(myTeam); // ❌ 返回 null
❌ 问题: 由于 Team
没有正确重写 hashCode()
,HashMap
无法找到对应的键值对。
✅ 解决方案: 重写 hashCode()
,保证相等的对象返回相同的哈希码:
@Override
public final int hashCode() {
int result = 17;
if (city != null) {
result = 31 * result + city.hashCode();
}
if (department != null) {
result = 31 * result + department.hashCode();
}
return result;
}
✅ 现在 leaders.get(myTeam)
就能正确返回 "Anne"
。
4. 何时重写 equals()
和 hashCode()
?
✅ 最佳实践: 要么两个方法都重写,要么都不重写。
- ✅ 实体类(Entity):如果对象有唯一标识(如数据库主键),可以使用默认实现
- ✅ 值对象(Value Object):如果对象的相等性由属性决定,应该重写这两个方法
还记得我们的 Money
类吗?55 USD 等于 55 USD,即使它们是两个不同的实例。
5. 实现辅助工具
手动实现这两个方法容易出错,推荐使用以下工具:
✅ IDE 自动生成: 大多数 IDE(如 IntelliJ IDEA、Eclipse)都支持一键生成 equals()
和 hashCode()
。
✅ 第三方库:
- Apache Commons Lang
- Google Guava
- Project Lombok 提供的
@EqualsAndHashCode
注解
@EqualsAndHashCode
class Money {
int amount;
String currencyCode;
}
✅ 可以看到,equals()
和 hashCode()
是“成对出现”的,甚至有一个共同的注解。
6. 验证契约
可以使用 EqualsVerifier 库来验证契约是否被正确实现。
✅ 添加 Maven 依赖:
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>3.15.3</version>
<scope>test</scope>
</dependency>
✅ 使用方式:
@Test
public void equalsHashCodeContracts() {
EqualsVerifier.forClass(Team.class).verify();
}
⚠️ 注意:EqualsVerifier 的默认配置比 Java SE 契约更严格,例如它要求字段不可变、方法不能抛出 NullPointerException
等。
7. 总结
本文总结了 equals()
和 hashCode()
的契约和最佳实践:
✅ 关键要点:
- 重写
equals()
时,必须同时重写hashCode()
- 对于值对象,务必重写这两个方法
- 避免通过继承破坏
equals()
的对称性 - 推荐使用 IDE 或第三方库自动生成
- 使用 EqualsVerifier 验证实现是否合规
📦 所有示例代码可在 GitHub 查看。