Contrats Java equals () et hashCode ()

1. Vue d'ensemble

Dans ce didacticiel, nous présenterons deux méthodes étroitement liées: equals () et hashCode () . Nous allons nous concentrer sur leurs relations les uns avec les autres, comment les remplacer correctement et pourquoi nous devrions remplacer les deux ou aucun.

2. égale ()

La classe Object définit à la fois les méthodes equals () et hashCode () - ce qui signifie que ces deux méthodes sont implicitement définies dans chaque classe Java, y compris celles que nous créons:

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

Nous nous attendrions à ce que revenu égal (dépenses) retourne vrai . Mais avec la classe Money dans sa forme actuelle, ce ne sera pas le cas.

L'implémentation par défaut de equals () dans la classe Object indique que l'égalité est la même chose que l'identité d'objet. Et les revenus et les dépenses sont deux exemples distincts.

2.1. Remplacer égal ()

Remplaçons la méthode equals () pour qu'elle ne considère pas uniquement l'identité de l'objet, mais aussi la valeur des deux propriétés pertinentes:

@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. est égal à () Contrat

Java SE définit un contrat que notre implémentation de la méthode equals () doit remplir. La plupart des critères relèvent du bon sens. La méthode equals () doit être:

  • réflexif : un objet doit s'égaliser
  • symétrique : x.equals (y) doit renvoyer le même résultat que y.equals (x)
  • transitive : si x.equals (y) et y.equals (z) alors aussi x.equals (z)
  • cohérent : la valeur de equals () ne devrait changer que si une propriété contenue dans equals () change (aucun caractère aléatoire n'est autorisé)

Nous pouvons rechercher les critères exacts dans la documentation Java SE pour la classe Object .

2.3. Violer la symétrie equals () avec l'héritage

Si le critère d' égalité () est un tel bon sens, comment pouvons-nous le violer du tout? Eh bien, les violations se produisent le plus souvent, si nous étendons une classe qui a remplacé equals () . Considérons une classe de bon qui étend notre classe Money :

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

À première vue, la classe Voucher et son remplacement pour equals () semblent être corrects. Et les deux méthodes equals () se comportent correctement tant que nous comparons Money to Money ou Voucher to Voucher . Mais que se passe-t-il si l'on compare ces deux objets?

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.

Cela viole les critères de symétrie du contrat equals () .

2.4. Correction de la symétrie égale () avec la composition

Pour éviter cet écueil, nous devons privilégier la composition à l'héritage.

Au lieu de sous- classer Money , créons une classe Voucher avec une propriété 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 }

Et maintenant, les égaux fonctionneront symétriquement comme l'exige le contrat.

3. hashCode ()

hashCode () retourne un entier représentant l'instance actuelle de la classe. Nous devons calculer cette valeur conformément à la définition de l'égalité pour la classe. Ainsi, si nous surchargons la méthode equals () , nous devons également surcharger hashCode () .

Pour plus de détails, consultez notre guide de hashCode () .

3.1. Contrat hashCode ()

Java SE définit également un contrat pour la méthode hashCode () . Un examen approfondi montre à quel point hashCode () et equals () sont étroitement liés .

Les trois critères du contrat de hashCode () mentionnent d'une certaine manière la méthode equals () :

  • cohérence interne : la valeur de hashCode () ne peut changer que si une propriété qui est dans equals () change
  • égale cohérence : les objets égaux les uns aux autres doivent renvoyer le même hashCode
  • collisions : les objets inégaux peuvent avoir le même hashCode

3.2. Violation de la cohérence de hashCode () et equals ()

Le 2ème critère du contrat des méthodes hashCode a une conséquence importante: si nous surchargons equals (), nous devons également surcharger hashCode (). Et c'est de loin la violation la plus répandue concernant les contrats des méthodes equals () et hashCode () .

Voyons un tel exemple:

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

La classe Team remplace uniquement equals () , mais elle utilise toujours implicitement l'implémentation par défaut de hashCode () telle que définie dans la classe Object . Et cela renvoie un hashCode () différent pour chaque instance de la classe. Cela viole la deuxième règle.

Maintenant, si nous créons deux objets Team , tous deux avec la ville «New York» et le département «marketing», ils seront égaux, mais ils renverront des hashCodes différents.

3.3. Clé HashMap avec un hashCode incohérent ()

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()
  • Remplacer equals () et hashCode () pour les objets de valeur
  • Soyez conscient des pièges de l'extension des classes qui ont remplacé equals () et hashCode ()
  • Envisagez d'utiliser un IDE ou une bibliothèque tierce pour générer les méthodes equals () et hashCode ()
  • Envisagez d'utiliser EqualsVerifier pour tester notre implémentation

Enfin, tous les exemples de code peuvent être trouvés sur GitHub.