Java е равна на () и hashCode () договори

1. Общ преглед

В този урок ще представим два метода, които са тясно свързани: equals () и hashCode () . Ще се съсредоточим върху връзката им помежду си, как правилно да ги заменим и защо трябва да заменим и двете, или нито едното, нито другото.

2. е равно ()

Класът 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 (разходи) , за да се върнат истинските . Но с класа Money в сегашния си вид няма.

Прилагането по подразбиране на equals () в класа Object казва, че равенството е същото като идентичността на обекта. А приходите и разходите са два отделни случая.

2.1. Замяна на равни ()

Нека заменим метода 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) 

2.2. е равно () Договор

Java SE дефинира договор, който нашето изпълнение на метода equals () трябва да изпълни. Повечето от критериите са здрав разум. Методът equals () трябва да бъде:

  • рефлексивен : обектът трябва да се равнява на себе си
  • симетрично : x.equals (y) трябва да върне същия резултат като y.equals (x)
  • преходно : ако x.equals (y) и y.equals (z), тогава и x.equals (z)
  • последователно : стойността на equals () трябва да се променя само ако свойство, което се съдържа в equals (), се променя (не е разрешена случайност)

Можем да потърсим точните критерии в Java SE Docs за клас Object .

2.3. Нарушаване на равно () симетрия с наследяване

Ако критерият за equals () е толкова здрав разум, как изобщо можем да го нарушим? Е, нарушенията се случват най-често, ако разширим клас, който е заменил equals () . Нека разгледаме клас на ваучер, който разширява класа ни Money :

class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o)  // other methods }

На пръв поглед класът на ваучерите и неговото заместване за equals () изглеждат правилни. И двата метода 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.

Това нарушава критериите за симетрия на договора equals () .

2.4. Фиксиране на равно () симетрия с композиция

За да избегнем тази клопка, трябва да предпочитаме композицията пред наследяването.

Вместо да подкласираме Money , нека създадем клас на ваучер със свойство 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)  // other methods }

И сега, равни ще работят симетрично, както изисква договорът.

3. hashCode ()

hashCode () връща цяло число, представляващо текущия екземпляр на класа. Трябва да изчислим тази стойност в съответствие с дефиницията за равенство за класа. По този начин, ако заменим метода equals () , ние също трябва да заменим hashCode () .

За повече подробности вижте нашето ръководство за hashCode () .

3.1. hashCode () Договор

Java SE също дефинира договор за метода hashCode () . Внимателният поглед върху него показва колко са тясно свързани hashCode () и 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 () , но все още използва имплицитно изпълнението по подразбиране на hashCode (), както е дефинирано в Object class. И това връща различен 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:

Map 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.

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.

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.

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

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

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 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.

5. Implementation Helpers

We typically don't write the implementation of these methods by hand. As can be seen, there are quite a few pitfalls.

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

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

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

6. Verifying the Contracts

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

Let's add the EqualsVerifier Maven test dependency:

 nl.jqno.equalsverifier equalsverifier 3.0.3 test 

Let's verify that our Team class follows the equals() and hashCode() contracts:

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

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

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, is final.

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. This adheres to a recommendation of Domain-Driven Design to make value objects immutable.

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

7. Conclusion

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

  • Always override hashCode() if we override equals()
  • Замяна на equals () и hashCode () за обекти на стойност
  • Имайте предвид капаните на разширяващите се класове, които са заменили equals () и hashCode ()
  • Помислете за използването на IDE или библиотека на трета страна за генериране на методите equals () и hashCode ()
  • Помислете за използването на EqualsVerifier, за да тествате нашето изпълнение

И накрая, всички примери за код могат да бъдат намерени в GitHub.