Contratti Java equals () e hashCode ()

1. Panoramica

In questo tutorial, introdurremo due metodi che appartengono strettamente insieme: equals () e hashCode () . Ci concentreremo sulla loro relazione reciproca, su come sovrascriverli correttamente e sul motivo per cui dovremmo ignorare entrambi o nessuno dei due.

2. è uguale a ()

La classe Object definisce entrambi i metodi equals () e hashCode () , il che significa che questi due metodi sono definiti implicitamente in ogni classe Java, comprese quelle che creiamo:

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

Ci aspetteremmo che reddito.equals (spese) tornasse vero . Ma con la classe Money nella sua forma attuale, non lo farà.

L'implementazione predefinita di equals () nella classe Object dice che l'uguaglianza è la stessa dell'identità dell'oggetto. E le entrate e le spese sono due casi distinti.

2.1. Sostituzione uguale a ()

Sostituiamo il metodo equals () in modo che non consideri solo l'identità dell'oggetto, ma anche il valore delle due proprietà rilevanti:

@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. è uguale a () Contratto

Java SE definisce un contratto che la nostra implementazione del metodo equals () deve soddisfare. La maggior parte dei criteri sono di buon senso. Il metodo equals () deve essere:

  • riflessivo : un oggetto deve eguagliare se stesso
  • simmetrico : x.equals (y) deve restituire lo stesso risultato di y.equals (x)
  • transitivo : se x.equals (y) e y.equals (z) allora anche x.equals (z)
  • coerente : il valore di equals () dovrebbe cambiare solo se una proprietà contenuta in equals () cambia (nessuna casualità consentita)

Possiamo cercare i criteri esatti nei documenti Java SE per la classe Object .

2.3. Violare equals () Symmetry With Inheritance

Se il criterio per equals () è tale buon senso, come possiamo violarlo? Bene, le violazioni si verificano più spesso, se estendiamo una classe che ha sovrascritto equals () . Consideriamo una classe Voucher che estende la nostra classe Money :

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

A prima vista, la classe Voucher e la sua sostituzione per equals () sembrano essere corrette. Ed entrambi i metodi equals () si comportano correttamente fintanto che confrontiamo Money to Money o Voucher to Voucher . Ma cosa succede se confrontiamo questi due oggetti?

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.

Ciò viola i criteri di simmetria del contratto equals () .

2.4. La correzione è uguale a () Simmetria con composizione

Per evitare questa trappola, dovremmo privilegiare la composizione rispetto all'eredità.

Invece di creare una sottoclasse Money , creiamo una classe Voucher con una proprietà 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 }

E ora, uguali funzionerà in modo simmetrico come richiesto dal contratto.

3. hashCode ()

hashCode () restituisce un numero intero che rappresenta l'istanza corrente della classe. Dovremmo calcolare questo valore coerentemente con la definizione di uguaglianza per la classe. Quindi, se sovrascriviamo il metodo equals () , dobbiamo anche sovrascrivere hashCode () .

Per ulteriori dettagli, consulta la nostra guida a hashCode () .

3.1. hashCode () Contratto

Java SE definisce anche un contratto per il metodo hashCode () . Uno sguardo approfondito mostra quanto siano strettamente correlati hashCode () ed equals () .

Tutti e tre i criteri nel contratto di hashCode () menzionano in qualche modo il metodo equals () :

  • coerenza interna : il valore di hashCode () può cambiare solo se una proprietà che è in equals () modifiche
  • è uguale a consistenza : gli oggetti uguali tra loro devono restituire lo stesso hashCode
  • collisioni : oggetti disuguali possono avere lo stesso hashCode

3.2. Violazione della coerenza di hashCode () ed equals ()

Il secondo criterio del contratto dei metodi hashCode ha un'importante conseguenza: se sovrascriviamo equals (), dobbiamo anche sovrascrivere hashCode (). E questa è di gran lunga la violazione più diffusa per quanto riguarda i contratti dei metodi equals () e hashCode () .

Vediamo un esempio del genere:

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

La classe Team sovrascrive solo equals () , ma utilizza ancora implicitamente l'implementazione predefinita di hashCode () come definito nella classe Object . E questo restituisce un hashCode () diverso per ogni istanza della classe. Ciò viola la seconda regola.

Ora se creiamo due oggetti Team , entrambi con città "New York" e dipartimento "marketing", saranno uguali, ma restituiranno codici hash diversi.

3.3. Chiave HashMap con un hashCode () incoerente

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()
  • Sostituisci equals () e hashCode () per gli oggetti valore
  • Sii consapevole delle trappole dell'estensione delle classi che hanno sovrascritto equals () e hashCode ()
  • Prendi in considerazione l'utilizzo di un IDE o di una libreria di terze parti per generare i metodi equals () e hashCode ()
  • Prendi in considerazione l'utilizzo di EqualsVerifier per testare la nostra implementazione

Infine, tutti gli esempi di codice possono essere trovati su GitHub.