Java equals() 和 hashCode() 契约
上次更新:2024 年 1 月 8 日
1. 概述
在本教程中,我们将介绍两个紧密相关的equals() 和 hashCode() 方法。我们将重点关注它们之间的关系、如何正确覆盖它们以及为什么应该同时覆盖两者或都不覆盖。
2. equals() 方法
默认情况下,Object 类定义了 equals() 和 hashCode() 方法。因此,每个 Java 类都隐式地拥有这两个方法。
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,但使用当前的Money 类实现,它不会。
Object 类中 equals() 的默认实现比较对象的身份。在我们的示例中,Money 类的 income 和 expenses 实例具有两个不同的身份。因此,使用 equals() 方法比较它们会返回 false。
要更改此行为,我们必须覆盖此方法。
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;
}
上面,我们有三个条件来检查 Money 实例是否与任何其他对象相同。首先,如果对象等于自身,它将返回 true。其次,如果它不是 Money 的实例,它将返回 false。第三,我们将它与另一个 Money 类实例的属性进行比较。详细地说,我们确保被比较类的所有属性与比较类的属性匹配。
2.2. equals() 契约
Java SE 定义了我们的 equals() 方法实现必须满足的契约。简而言之,大多数标准遵循常识,但我们可以定义 equals() 方法必须遵循的正式规则。它必须是
- 自反性:一个对象必须等于自身
- 对称性:x.equals(y) 必须返回与 y.equals(x) 相同的结果
- 传递性:如果 x.equals(y) 且 y.equals(z),则 x.equals(z) 也必须成立
- 一致性:equals() 的值应该只在 equals() 中包含的属性发生变化时才发生变化(不允许随机性)
我们可以在 Java SE 文档的 Object 类 中查阅确切的标准。
2.3. 通过继承违反 equals() 对称性
如果 equals() 的标准如此常识,那么我们如何违反它呢?好吧,违反 equals() 契约更有可能发生在扩展已经覆盖了 equals() 方法的类时。让我们考虑一个扩展我们 Money 类的 Voucher 类
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;
}
// other methods
}
乍一看,Voucher 类及其对 equals() 的覆盖似乎是正确的。只要我们将 Money 与 Money 或 Voucher 与 Voucher 进行比较,这两个 equals() 方法都会正确行为。但是,如果我们将这两个对象进行比较会发生什么?
Money cash = new Money(42, "USD");
WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon");
voucher.equals(cash) => false // As expected.
cash.equals(voucher) => true // That's wrong.
因此,我们违反了对称性标准。
2.4。使用组合修复 equals() 对称性
为了避免犯错,我们应该优先使用组合而不是继承。
与其继承 Money,不如创建一个带有 Money 属性的 Voucher 类
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;
}
// other methods
}
现在 .equals() 将按合同要求对称地工作。
3. .hashCode() 方法
.hashCode() 方法返回一个整数,代表类的当前实例。我们应该根据类定义的相等性一致地计算此值。
有关更多详细信息,请查看我们的 .hashCode() 指南。
3.1. .hashCode() 合同
Java SE 也定义了 .hashCode() 方法的合同。对该合同的透彻研究揭示了 .hashCode() 和 .equals() 之间的密切关系。
合同中的所有三个标准都以某种方式提到了 .equals() 方法:
- 内部一致性:hashCode() 的值只能在 equals() 中的属性发生变化时才会改变
- 相等一致性:彼此相等的对象必须返回相同的 hashCode
- 冲突:不相等的对象可能有相同的 hashCode
3.2. 违反 hashCode() 和 equals() 的一致性
.hashCode() 合同的第二个标准具有重要后果:如果我们覆盖了 equals(),我们也必须覆盖 hashCode()。这是关于 equals() 和 hashCode() 方法合同最常见的违规行为。
让我们看一个例子
class Team {
String city;
String department;
@Override
public final boolean equals(Object o) {
// implementation
}
}
Team 类仅覆盖了 equals(),但它仍然隐式地使用 Object 类中定义的 hashCode() 的默认实现。因此,它将为类的每个实例返回一个不同的 hashCode() 并违反第二个规则。
现在,如果我们创建两个 Team 对象,它们都具有城市“New York”和部门“marketing”,它们将相等,但它们将返回不同的 hashCodes。
3.3. HashMap 键的 hashCode() 不一致
但是,Team 类中的合同违规行为有什么问题?嗯,问题开始于涉及到一些基于哈希的集合时。让我们尝试将我们的 Team 类用作 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);
我们期望 myTeamLeader 返回“Anne”,但使用当前代码,它没有。
如果我们想将 Team 类的实例用作 HashMap 键,我们必须覆盖 hashCode() 方法,使其符合合同;相等的对象返回相同的 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()?
通常,我们希望覆盖 .equals() 和 .hashCode() 两者或两者都不覆盖。 我们在第 3 节中看到了忽略此规则的负面后果。
领域驱动设计可以帮助我们决定何时应该保持它们不变。对于实体类,对于具有内在标识的对象,默认实现通常是合理的。
但是,对于值对象,我们通常更喜欢基于它们的属性进行相等性比较。因此,我们希望覆盖 .equals() 和 .hashCode()。记住我们在第 2 节中的 Money 类:55 美元等于 55 美元,即使它们是两个独立的实例。
5. 实现帮助器
我们通常不会手动编写这些方法的实现。如我们所见,有很多陷阱。
一个常见的选项是 让我们的 IDE 生成 .equals() 和 .hashCode() 方法。
Apache Commons Lang 和 Google Guava 具有辅助类,可以简化使用这两种方法的编写。
Project Lombok 还提供了一个 @EqualsAndHashCode 注解。 再次注意 .equals() 和 .hashCode() “协同工作”,甚至具有一个通用注解。
6. 验证契约
如果我们想检查我们的实现是否符合 Java SE 契约和最佳实践,我们可以使用 EqualsVerifier 库。
让我们添加 EqualsVerifier Maven 测试依赖
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>3.15.3</version>
<scope>test</scope>
</dependency>
现在让我们验证我们的 Team 类是否遵循 equals() 和 hashCode() 契约
@Test
public void equalsHashCodeContracts() {
EqualsVerifier.forClass(Team.class).verify();
}
值得注意的是,EqualsVerifier 会测试 equals() 和 hashCode() 方法。
EqualsVerifier 比 Java SE 契约更严格。 例如,它确保我们的方法不会抛出 NullPointerException。 此外,它强制要求这两个方法,或者类本身,都是 final 的。
重要的是要意识到,EqualsVerifier 的默认配置仅允许不可变字段。 这比 Java SE 契约允许的检查更严格。 它遵循领域驱动设计的建议,使值对象不可变。
如果我们发现一些内置的约束是不必要的,我们可以添加一个 suppress(Warning.SPECIFIC_WARNING) 到我们的 EqualsVerifier 调用中。
7. 结论
在本文中,我们讨论了 equals() 和 hashCode() 契约。 我们应该记住
- 始终在重写 equals() 时重写 hashCode()
- 为值对象重写 equals() 和 hashCode()
- 注意扩展已经重写了 equals() 和 hashCode() 的类的陷阱
- 考虑使用 IDE 或第三方库来生成 equals() 和 hashCode() 方法
- 考虑使用 EqualsVerifier 来测试我们的实现
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















