Java este egal cu () și contracte hashCode ()

1. Prezentare generală

În acest tutorial, vom introduce două metode care aparțin strâns: equals () și hashCode () . Ne vom concentra pe relația lor unul cu celălalt, cum să le înlocuim corect și de ce ar trebui să le ignorăm pe ambele sau pe niciuna dintre ele.

2. egal ()

The Object definește clasa ambele egal () și hashCode () metode - ceea ce înseamnă că aceste două metode sunt definite în mod implicit în fiecare clasă Java, inclusiv cele pe care le creăm:

class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD"); Money expenses = new Money(55, "USD"); boolean balanced = income.equals(expenses)

Ne-am aștepta ca veniturile.egale (cheltuieli) să revină adevărat . Dar cu clasa Money în forma sa actuală, nu o va face.

Implementarea implicită a equals () în clasa Object spune că egalitatea este aceeași cu identitatea obiectului. Iar veniturile și cheltuielile sunt două cazuri distincte.

2.1. Suprascriere egală cu ()

Să anulăm metoda equals () astfel încât să nu ia în considerare doar identitatea obiectului, ci mai degrabă și valoarea celor două proprietăți relevante:

@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. egal () Contract

Java SE definește un contract pe care trebuie să îl îndeplinim implementarea noastră a metodei equal () . Majoritatea criteriilor sunt de bun simț. Metoda equal () trebuie să fie:

  • reflexiv : un obiect trebuie să se egalizeze
  • simetric : x.equals (y) trebuie să returneze același rezultat ca y.equals (x)
  • tranzitiv : dacă x.equals (y) și y.equals (z) atunci și x.equals (z)
  • consecvent : valoarea lui equal () ar trebui să se schimbe numai dacă o proprietate care este conținută în equal () se schimbă (nu este permisă aleatoritatea)

Putem căuta criteriile exacte în documentele Java SE Docs pentru clasa Object .

2.3. Încălcarea egalului () Simetrie cu moștenire

Dacă criteriul pentru egal () este un bun simț, cum îl putem încălca deloc? Ei bine, încălcările se întâmplă cel mai adesea, dacă extindem o clasă care a depășit egal () . Să luăm în considerare o clasă Voucher care extinde clasa noastră de bani :

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

La prima vedere, clasa Voucher și suprascrierea sa pentru equal () par a fi corecte. Și ambele metode equal () se comportă corect atâta timp cât comparăm Money to Money sau Voucher to Voucher . Dar ce se întâmplă dacă comparăm aceste două obiecte?

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.

Aceasta încalcă criteriile de simetrie ale contractului egal () .

2.4. Fixing equals () Simetrie cu compoziție

Pentru a evita această capcană, ar trebui să favorizăm compoziția în locul moștenirii.

În loc să subclasăm Money , să creăm o clasă Voucher cu o proprietate 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 }

Și acum, egalii vor funcționa simetric așa cum prevede contractul.

3. hashCode ()

hashCode () returnează un număr întreg reprezentând instanța curentă a clasei. Ar trebui să calculăm această valoare în concordanță cu definiția egalității pentru clasă. Astfel, dacă anulăm metoda equals () , trebuie să suprascriem și hashCode () .

Pentru mai multe detalii, consultați ghidul nostru pentru hashCode () .

3.1. hashCode () Contract

Java SE definește, de asemenea, un contract pentru metoda hashCode () . O privire amănunțită la ea arată cât de strâns legate de hashCode () și egali () sunt.

Toate cele trei criterii din contractul hashCode () menționează în anumite moduri metoda egal () :

  • consistență internă : valoarea hashCode () se poate schimba numai în cazul în care o proprietate care este în equals () modificări
  • este egală cu consistența : obiectele care sunt egale între ele trebuie să returneze același hashCode
  • coliziuni : obiectele inegale pot avea același hashCode

3.2. Încălcarea coerenței hashCode () și egal ()

Al doilea criteriu al contractului de metode hashCode are o consecință importantă: Dacă suprascriem egal (), trebuie să suprascriem și hashCode (). Și aceasta este de departe cea mai răspândită încălcare în ceea ce privește contractele metodelor equal () și hashCode () .

Să vedem un astfel de exemplu:

class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }

De Echipa suprareglările de clasă numai egal cu () , dar încă mai folosește implicit punerea în aplicare implicit a hashCode () , astfel cum sunt definite în Object clasa. Și aceasta returnează un hashCode diferit () pentru fiecare instanță a clasei. Aceasta încalcă a doua regulă.

Acum, dacă creăm două obiecte Team , ambele cu orașul „New York” și departamentul „marketing”, acestea vor fi egale, dar vor returna coduri hash diferite.

3.3. Cheie HashMap cu un cod hash incoerent ()

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()
  • Override equals () și hashCode () pentru obiectele de valoare
  • Fiți conștienți de capcanele clasei extinse care au suprascris egal () și hashCode ()
  • Luați în considerare utilizarea unui IDE sau a unei biblioteci terță parte pentru generarea metodelor equals () și hashCode ()
  • Luați în considerare utilizarea EqualsVerifier pentru a testa implementarea noastră

În cele din urmă, toate exemplele de cod pot fi găsite pe GitHub.