Java equals() and hashCode() Contracts – Java equals()和hashCode()合同

最后修改: 2018年 11月 28日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll introduce two methods that closely belong together: equals() and hashCode(). We’ll focus on their relationship with each other, how to correctly override them, and why we should override both or neither.

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

2. equals()

2.equals()

The Object class defines both the equals() and hashCode() methods, which means that these two methods are implicitly defined in every Java class, including the ones we create:

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)

We would expect income.equals(expenses) to return true, but with the Money class in its current form, it won’t.

我们希望income.equals(expenses)能够返回true,,但是在Money类目前的形式下,它不会。

The default implementation of equals() in the Object class says that equality is the same as object identity, and income and expenses are two distinct instances.

Object类中equals()的默认实现表示平等与对象身份相同,并且incomeexpenses是两个不同的实例。

2.1. Overriding equals()

2.1.重写equals()

Let’s override the equals() method so that it doesn’t consider only object identity, but also the value of the two relevant properties:

让我们重写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;
}

2.2. equals() Contract

2.2.equals() 合同

Java SE defines the contract that our implementation of the equals() method must fulfill. Most of the criteria are common sense. The equals() method must be:

Java SE定义了我们对equals()方法的实现必须满足的契约。大多数标准都是常识。 equals()方法必须是。

  • reflexive: an object must equal itself
  • symmetric: x.equals(y) must return the same result as y.equals(x)
  • transitive: if x.equals(y) and y.equals(z), then also x.equals(z)
  • consistent: the value of equals() should change only if a property that is contained in equals() changes (no randomness allowed)

We can look up the exact criteria in the Java SE Docs for the Object class.

我们可以在Java SE Docs中查找Object类的确切标准

2.3. Violating equals() Symmetry With Inheritance

2.3.违反equals()继承的对称性

If the criteria for equals() is such common sense, then how can we violate it at all? Well, violations happen most often if we extend a class that has overridden equals(). Let’s consider a Voucher class that extends our Money class:

如果equals()的标准是这样的常识,那么我们怎么可能违反它呢?好吧,如果我们扩展了一个重写了equals()的类,那么违反行为就会经常发生。让我们考虑一个Voucher类,它扩展了我们的Money类。

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
}

At first glance, the Voucher class and its override for equals() seem to be correct. And both equals() methods behave correctly as long as we compare Money to Money or Voucher to Voucher. But what happens, if we compare these two objects:

乍一看,Voucher类和它对equals()的重写似乎是正确的。只要我们将MoneyMoneyVoucherVoucher进行比较,这两个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.

That violates the symmetry criteria of the equals() contract.

这违反了equals()合约的对称性标准

2.4. Fixing equals() Symmetry With Composition

2.4.用构图修复equals()的对称性

To avoid this pitfall, we should favor composition over inheritance.

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

Instead of subclassing Money, let’s create a Voucher class with a Money property:

我们不对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
}

Now equals will work symmetrically as the contract requires.

现在等价物将按照合同要求对称地工作。

3. hashCode()

3.hashCode()

hashCode() returns an integer representing the current instance of the class. We should calculate this value consistent with the definition of equality for the class. Thus, if we override the equals() method, we also have to override hashCode().

hashCode()返回一个代表该类当前实例的整数。我们应该按照该类的平等定义来计算这个值。因此,如果我们覆盖equals()方法,我们也必须覆盖hashCode()

For more details, check out our guide to hashCode().

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

3.1. hashCode() Contract

3.1.hashCode()合同

Java SE also defines a contract for the hashCode() method. A thorough look at it shows how closely related hashCode() and equals() are.

Java SE还为hashCode()方法定义了一个契约。彻底看一下,就会发现hashCode()equals()的关系是多么密切。

All three criteria in the hashCode() contract mention the equals() method in some way:

hashCode()合约中的三个标准都以某种方式提到了equals()方法:

  • internal consistency: the value of hashCode() may only change if a property that is in equals() changes
  • equals consistency: objects that are equal to each other must return the same hashCode
  • collisions: unequal objects may have the same hashCode

3.2. Violating the Consistency of hashCode() and equals()

3.2.违反hashCode()equals()的一致性

The second criteria of the hashCode methods contract has an important consequence: If we override equals(), we must also override hashCode(). This is by far the most widespread violation regarding the equals() and hashCode() methods contracts.

hashCode方法契约的第二个标准有一个重要的后果。如果我们覆盖equals(),我们也必须覆盖hashCode()这是迄今为止关于equals()hashCode()方法契约最普遍的违反。

Let’s see such an example:

让我们看看这样一个例子。

class Team {

    String city;
    String department;

    @Override
    public final boolean equals(Object o) {
        // implementation
    }
}

The Team class overrides only equals(), but it still implicitly uses the default implementation of hashCode() as defined in the Object class. And this returns a different hashCode() for every instance of the class. This violates the second rule.

Team类只重写了equals(),但它仍然隐含地使用Object类中定义的hashCode()的默认实现。而这对该类的每个实例都会返回不同的hashCode()这违反了第二条规则。

Now, if we create two Team objects, both with city “New York” and department “marketing,” they will be equal, but they’ll return different hashCodes.

现在,如果我们创建两个Team对象,都是城市 “纽约 “和部门 “营销”,它们将是相等的,但它们将返回不同的哈希代码。

3.3. HashMap Key With an Inconsistent hashCode()

3.3.HashMap键与一个不一致的hashCode()

But why is the contract violation in our Team class a problem? Well, the trouble starts when some hash-based collections are involved. Let’s try to use our Team class as a key of a HashMap:

但是为什么我们的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);

We would expect myTeamLeader to return “Anne,” but with the current code, it doesn’t.

我们希望myTeamLeader能返回 “Anne”,但在目前的代码中,它并没有。

If we want to use instances of the Team class as HashMap keys, we have to override the hashCode() method so that it adheres to the contract; equal objects return the same hashCode.

如果我们想使用Team类的实例作为HashMap键,我们必须覆盖hashCode()方法,使其遵守契约;相等的对象返回相同的hashCode。

Let’s see an example implementation:

让我们看看一个实现的例子。

@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;
}

After this change, leaders.get(myTeam) returns “Anne” as expected.

在这一改变之后,leaders.get(myTeam)按照预期返回 “Anne”。

4. When Do We Override equals() and hashCode()?

4.我们何时覆盖equals()hashCode()

Generally, we want to override either both of them or neither of them. We just saw in Section 3 the undesired consequences if we ignore this rule.

通常情况下,我们希望覆盖它们中的任何一个或任何一个。我们刚刚在第3节中看到,如果我们忽视了这一规则,就会产生不良后果。

Domain-Driven Design can help us decide circumstances when we should leave them be. For entity classes, for objects having an intrinsic identity, the default implementation often makes sense.

领域驱动设计可以帮助我们决定在什么情况下应该让它们保持不变。对于实体类,对于具有内在身份的对象,默认的实现往往是有意义的。

However, for value objects, we usually prefer equality based on their properties. Thus, we want to override equals() and hashCode(). Remember our Money class from Section 2: 55 USD equals 55 USD, even if they’re two separate instances.

然而,对于值对象,我们通常更喜欢基于其属性的平等。因此,我们要覆盖equals()hashCode()。还记得第二节中我们的Money类:55美元等于55美元,即使它们是两个独立的实例。

5. Implementation Helpers

5.实施帮助

We typically don’t write the implementation of these methods by hand. As we’ve seen, there are quite a few pitfalls.

我们通常不会手工编写这些方法的实现。正如我们所看到的,有相当多的隐患。

One common option is to let our IDE generate the equals() and hashCode() methods.

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

Apache Commons Lang and Google Guava have helper classes in order to simplify writing both methods.

Apache Commons LangGoogle Guava有帮助类,以便简化这两种方法的编写。

Project Lombok also provides an @EqualsAndHashCode annotation. Note again how equals() and hashCode() “go together” and even have a common annotation.

Project Lombok也提供了一个@EqualsAndHashCode注释。再次注意到equals()hashCode()是如何 “走在一起”,甚至有一个共同的注解。

6. Verifying the Contracts

6.核实合同

If we want to check whether our implementations adhere to the Java SE contracts, and also to best practices, we can use the EqualsVerifier library.

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

Let’s add the EqualsVerifier Maven test dependency:

让我们添加EqualsVerifier的Maven测试依赖。

<dependency>
    <groupId>nl.jqno.equalsverifier</groupId>
    <artifactId>equalsverifier</artifactId>
    <version>3.0.3</version>
    <scope>test</scope>
</dependency>

Now let’s verify that our Team class follows the equals() and hashCode() contracts:

现在让我们验证一下我们的Team类是否遵循equals()hashCode()合约。

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

It’s worth noting that EqualsVerifier tests both the equals() and hashCode() methods.

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

EqualsVerifier is much stricter than the Java SE contract. For example, it makes sure that our methods can’t throw a NullPointerException. Also, it enforces that both methods, or the class itself, are final.

EqualsVerifier比Java SE的契约要严格得多。例如,它确保我们的方法不能抛出NullPointerException。而且,它还强制要求两个方法或类本身都是最终的。

It’s important to realize that the default configuration of EqualsVerifier allows only immutable fields. This is a stricter check than what the Java SE contract allows. It adheres to a recommendation of Domain-Driven Design to make value objects immutable.

重要的是要认识到,EqualsVerifier的默认配置只允许不可变的字段。这是一个比Java SE合同所允许的更严格的检查。它遵循领域驱动设计的建议,使值对象不可变。

If we find some of the built-in constraints unnecessary, we can add a suppress(Warning.SPECIFIC_WARNING) to our EqualsVerifier call.

如果我们发现一些内置的约束没有必要,我们可以在我们的suppress(Warning.SPECIFIC_WARNING)调用中加入EqualsVerifier

7. Conclusion 

7.结论

In this article, we discussed the equals() and hashCode() contracts. We should remember to:

在这篇文章中,我们讨论了equals()hashCode()合约。我们应该记住。

  • Always override hashCode() if we override equals()
  • Override equals() and hashCode() for value objects
  • Be aware of the traps of extending classes that have overridden equals() and hashCode()
  • Consider using an IDE or a third-party library for generating the equals() and hashCode() methods
  • Consider using EqualsVerifier to test our implementation

Finally, all code examples can be found over on GitHub.

最后,所有的代码实例都可以在GitHub上找到