Java最高

通过。开始使用Spring 5和Spring Boot 2学习的春天课程:

>>查看课程

1.概述

在本教程中,我们将介绍两个紧密相连的方法:equals ()hashCode ()。我们将关注它们之间的关系,如何正确地覆盖它们,以及为什么我们应该覆盖两者或两者都不覆盖。

2.equals ()

对象类定义了equals ()hashCode ()方法——这意味着这两个方法隐式地定义在每个Java类中,包括我们创建的类:

类金钱{int数量;字符串currencyCode;}
货币收益=新货币(55,“美元”);货币费用=新货币(55,“美元”);布尔值balanced = income.equals(费用)

我们希望income.equals(费用)返回真正的。但随着类的当前形式,它不会。

的默认实现equals ()在课堂上对象说平等和客体同一性是一样的。和收入费用是两个截然不同的例子。

2.1。压倒一切的equals ()

让我们重写equals ()方法,这样它不仅考虑对象的标识,而且还考虑两个相关属性的值:

@Override public boolean equals(Object o){如果(o == this)返回true;如果(!(o instanceof Money))返回false;Money other =(钱)o;boolean currencyCodeEquals =(这个。currencyCode == null &&其他。currencyCode == null) currencyCode == nullcurrencyCode != null && this.currencyCode.equals(other.currencyCode));返回。数量= =。& & currencyCodeEquals数量; }

2.2。equals ()合同

Java SE定义了一个契约,我们实现了equals ()方法必须履行。大多数标准都是常识。equals ()方法必须:

  • 反射性的:对象必须等于自身
  • 对称的:x.e quals (y)必须返回与y.equals (x)
  • 传递:如果x.e quals (y)y.equals (z)然后还x.e quals (z)
  • 一致的:的值equals ()只有当一个属性包含在equals ()改变(不允许随机性)

我们可以在Java SE Docs for the对象

2.3。违反equals ()对称与继承

如果equals ()这是常识,我们怎么能违背它呢?好吧,如果我们扩展一个已覆盖的类,则违反规则的情况最常见equals ()让我们考虑一个凭证类扩展了类:

class WrongVoucher extends Money {private String store;@Override public boolean equals(Object o){如果(o == this)返回true;如果(!(错误凭证实例))返回false;其他=(错误凭证)o;boolean currencyCodeEquals =(这个。currencyCode == null &&其他。currencyCode == null) currencyCode == nullcurrencyCode != null && this.currencyCode.equals(other.currencyCode));boolean storeEquals =(这个。store == null && other。store == null) (this. store == null)store != null && this.store.equals(other.store)); return this.amount == other.amount && currencyCodeEquals && storeEquals; } // other methods }

乍一看,是凭证类及其重写equals ()似乎是正确的。和两个equals ()只要我们进行比较,方法就会正常运行凭证凭证但是如果我们比较这两个物体,会发生什么呢?

Money cash = new Money(42, "USD");WrongVoucher voucher =新的WrongVoucher(42,“USD”,“Amazon”);voucher.equals(cash) => false //与预期一致。cash.equals(voucher) => true //错了。

这违反了对称准则equals ()合同。

2.4。修复equals ()对称与组成

为了避免这个陷阱,我们应该偏爱组合而不是继承。

而不是子类化,让我们创建一个凭证属性:

类凭证{私人货币价值;私人的字符串存储;(int amount, String currencyCode, String store) {this。value =新货币(amount, currencyCode);这一点。商店=商店;} @Override public boolean equals(Object o){如果(o == this)返回true;如果(!(凭证实例))返回false;凭证other =(凭证)o;布尔值= =(这个。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 }

现在,=将会按照合同的要求对称地工作。

3.hashCode ()

hashCode ()返回表示类的当前实例的整数。我们应该根据类的相等定义来计算这个值。因此如果我们重写equals ()方法,我们也必须重写hashCode ()

欲了解更多细节,请查看我们的指南hashCode ()

3.1。hashCode ()合同

Java SE还为hashCode ()方法。仔细观察一下就会发现它们之间的联系是多么紧密hashCode ()equals ()是这样的。

这三个标准都在合同中hashCode ()在某些方面提到equals ()方法:

  • 内部一致性:的值hashCode ()只有当属性在equals ()变化
  • =一致性:彼此相等的对象必须返回相同的hashCode
  • 碰撞:不同的对象可能具有相同的hashCode

3.2。违反hashCode ()equals ()

hashCode方法合同的第二个标准有一个重要的结果:如果重载equals(),也必须重载hashCode()。这是迄今为止最普遍的违反合同的行为equals ()hashCode ()方法。

让我们来看一个例子:

class Team {String city;字符串;@Override public final boolean equals(Object o){//实现}}

团队类只覆盖equals (),但它仍然隐式地使用的默认实现hashCode ()根据对象类。这个返回一个不同的hashCode ()对于类的每个实例。这违反了第二条规则。

现在如果我们创建两个团队对象,城市“New York”和部门“marketing”,它们是相等的,但是它们会返回不同的hashcode。

3.3。HashMap按键不一致hashCode ()

但是为什么我们的合同被违反了团队类问题?当涉及到一些基于哈希的集合时,麻烦就开始了。我们试着用团队类作为一个键HashMap:

Map leaders = new HashMap<>();领导人。put(new Team(“new York”、“development”)、“Anne”);领导人。put(新团队(“Boston”、“development”)、“Brian”);领导人。put(新团队(“波士顿”、“营销”)、“查理”);Team myTeam = new Team(" new York", "development");String myTeamLeader = leaders.get(myTeam);

我们希望myTeamLeader返回“安妮”。但对于当前的代码,它不需要。

类的实例团队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 ();}返回结果;}

这种变化后,leaders.get(麦田)按预期返回“Anne”。

4.什么时候重载equals ()hashCode ()吗?

一般来说,我们想要覆盖它们中的任何一个,或者两个都不覆盖。我们已经在第3节中看到了忽略该规则会带来的不希望的后果。

领域驱动的设计可以帮助我们决定什么时候应该放弃它们。对于实体类——对于具有内在标识的对象——默认实现通常是有意义的。

然而,对于值对象,我们通常倾向于根据它们的属性选择相等。因此想要覆盖equals ()hashCode ()。还记得我们Section 2中的class: 55 USD = 55 USD -即使它们是两个单独的实例。

5.实现助手

我们通常不会手工编写这些方法的实现。可以看到,有相当多的陷阱。

一个常见的方法是让我们的IDE生成equals ()hashCode ()方法。

Apache Commons Lang谷歌番石榴使用helper类来简化这两种方法的编写。

Project Lombok还提供了一个@EqualsAndHashCode注释。注意又如何equals ()hashCode ()“一起走”甚至有一个共同的注解。

6.确认合同

如果我们想检查我们的实现是否遵守Java SE契约以及一些最佳实践,我们可以使用EqualsVerifier库。

让我们加入EqualsVerifierMaven测试依赖关系:

<依赖> < groupId > nl.jqno。=verifier equalsverifier 3.0.3 test 

我们来验证一下团队班,equals ()hashCode ()合同:

@Test public void equalsHashCodeContracts() {EqualsVerifier.forClass(Team.class).verify();}

值得注意的是EqualsVerifier测试的equals ()hashCode ()方法。

EqualsVerifier比Java SE契约严格得多。例如,它确保我们的方法不会抛出NullPointerException。此外,它强制这两个方法或类本身都是final。

认识到这一点很重要的默认配置EqualsVerifier只允许不可变字段。这比Java SE契约所允许的检查更为严格。这符合领域驱动设计的建议,使值对象成为不可变的。

如果我们发现一些内置的约束没有必要,我们可以添加一个抑制(Warning.SPECIFIC_WARNING)对我们的EqualsVerifier调用。

7.结论

在本文中,我们讨论了equals ()hashCode ()合同。我们应该记住:

  • 总是覆盖hashCode ()如果我们覆盖equals ()
  • 覆盖equals ()hashCode ()的值对象
  • 要注意扩展已覆盖的类的陷阱equals ()hashCode ()
  • 考虑使用IDE或第三方库来生成equals ()hashCode ()方法
  • 考虑使用EqualsVerifier来测试我们的实现

最后,可以找到所有的代码示例在GitHub

Java底部

通过。开始使用Spring 5和Spring Boot 2学习的春天课程:

>>查看课程
本文评论关闭!