Java är lika med () och hashCode () -kontrakt

1. Översikt

I den här handledningen introducerar vi två metoder som hör nära varandra: lika () och hashCode () . Vi fokuserar på deras förhållande till varandra, hur man korrekt åsidosätter dem och varför vi ska åsidosätta båda eller varken.

2. är lika med ()

De Object klassen definierar både likhets () och hashkod () metoder - vilket innebär att dessa två metoder implicit definieras i varje Java-klass, inklusive de som vi skapar:

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

Vi förväntar income.equals (kostnader) för att återvända sant . Men med Money- klassen i sin nuvarande form kommer den inte att göra det.

Standardimplementeringen av lika () i klassen Objekt säger att jämlikhet är densamma som objektidentitet. Och intäkter och kostnader är två olika fall.

2.1. Åsidosättande är lika med ()

Låt oss åsidosätta metoden equals () så att den inte bara tar hänsyn till objektidentitet utan snarare också värdet på de två relevanta egenskaperna:

@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. är lika med () Kontrakt

Java SE definierar ett kontrakt som vår implementering av metoden equals () måste uppfylla. De flesta kriterierna är sunt förnuft. De equals () metoden måste vara:

  • reflexiv : ett objekt måste vara lika med sig själv
  • symmetrisk : x.equals (y) måste returnera samma resultat som y.equals (x)
  • transitive : om x.equals (y) och y.equals (z) då också x.equals (z)
  • konsekvent : värdet på lika () bör endast ändras om en egenskap som ingår i lika () ändras (ingen slumpmässighet tillåten)

Vi kan slå upp de exakta kriterierna i Java SE Docs för Object- klassen.

2.3. Överträdelse är lika med () Symmetri med arv

Om kriterierna för lika () är så sunt förnuft, hur kan vi alls bryta mot det? Brott inträffar oftast om vi utökar en klass som har åsidosatt lika () . Låt oss överväga en kupongklass som utökar vår pengaklass :

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

Vid första anblicken verkar Voucher- klassen och dess åsidosättning för lika () vara korrekt. Och båda lika () -metoderna beter sig korrekt så länge vi jämför pengar med pengar eller kupong med kupong . Men vad händer om vi jämför dessa två objekt?

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.

Det bryter mot symmetri-kriterierna i kontraktet lika () .

2.4. Fixing equals () Symmetry With Composition

För att undvika denna fallgrop bör vi föredra komposition framför arv.

I stället för att subklassera pengar , låt oss skapa en kupongklass med en Money- egendom:

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 }

Och nu kommer lika arbeta symmetriskt enligt kontraktet.

3. hashCode ()

hashCode () returnerar ett heltal som representerar den aktuella instansen av klassen. Vi bör beräkna detta värde i enlighet med definitionen av jämlikhet för klassen. Således om vi åsidosätter metoden equals () måste vi också åsidosätta hashCode () .

För mer information, kolla in vår guide till hashCode () .

3.1. hashCode () -kontrakt

Java SE definierar också ett kontrakt för hashCode () -metoden. En grundlig titt på den visar hur nära relaterade hashCode () och lika () är.

Alla tre kriterierna i kontraktet med hashCode () nämner på vissa sätt metoden () :

  • intern konsistens : värdet på hashCode () får bara ändras om en egenskap som är lika med () ändras
  • är lika med konsistens : objekt som är lika med varandra måste returnera samma hashCode
  • kollisioner : Ojämlika objekt kan ha samma hashCode

3.2. Kränka konsekvensen av hashCode () och lika med ()

Det andra kriteriet i kontraktet hashCode-metoder har en viktig konsekvens: Om vi ​​åsidosätter lika () måste vi också åsidosätta hashCode (). Och detta är överlägset den mest utbredda överträdelsen när det gäller kontrakt för metoderna equals () och hashCode () .

Låt oss se ett sådant exempel:

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

De Team klass åsidosätter endast lika () , men det är fortfarande underförstått använder standard genomförandet av hashkod () enligt definitionen i Object klassen. Och detta returnerar en annan hashCode () för varje förekomst av klassen. Detta bryter mot den andra regeln.

Om vi ​​nu skapar två Team- objekt, båda med staden "New York" och avdelningen "marketing", kommer de att vara lika, men de kommer att returnera olika hashCodes.

3.3. HashMap- nyckel med en inkonsekvent 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()
  • Åsidosätta lika med () och hashCode () för värdeobjekt
  • Var medveten om fällorna för att utvidga klasser som har åsidosatt lika () och hashCode ()
  • Överväg att använda en IDE eller ett tredjepartsbibliotek för att generera metoderna equals () och hashCode ()
  • Överväg att använda EqualsVerifier för att testa vårt genomförande

Slutligen kan alla kodexempel hittas på GitHub.