Java equals () et hashCode () Contrats

Java equals () et hashCode () Contrats

1. Vue d'ensemble

Dans ce didacticiel, nous présenterons deux méthodes étroitement liées:equals() ethashCode(). 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. equals()

La classeObject définit à la fois les méthodesequals() ethashCode() - 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 queincome.equals(expenses) renvoietrue. Mais avec la classeMoney dans sa forme actuelle, ce ne sera pas le cas.

L'implémentation par défaut deequals() dans la classeObject indique que l'égalité est la même que l'identité d'objet. Etincome etexpenses sont deux instances distinctes.

2.1. Remplacerequals()

Remplaçons la méthodeequals() afin qu'elle ne considère pas uniquement l'identité de l'objet, mais également 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)
      || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
    return this.amount == other.amount && currencyCodeEquals;
}

2.2. Contratequals()

Java SE définit un contrat que notre implémentation de la méthodeequals() doit remplir. Most of the criteria are common sense. La méthodeequals() doit être:

  • reflexive: un objet doit s'égaliser

  • symmetric:x.equals(y) must return the same result as y.equals(x)

  • transitive: six.equals(y) ety.equals(z) alors aussix.equals(z)

  • consistent: la valeur deequals() ne doit changer que si une propriété contenue dansequals() change (aucun caractère aléatoire autorisé)

Nous pouvons rechercher les critères exacts dans lesJava SE Docs for the Object class.

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

Si le critère pourequals() relève du bon sens, comment pouvons-nous le violer? Eh bien,violations happen most often, if we extend a class that has overridden equals(). Considérons une classeVoucher qui étend notre classeMoney:

class WrongVoucher extends Money {

    private String store;

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof WrongVoucher))
            return false;
        WrongVoucher other = (WrongVoucher)o;
        boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
          || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
        boolean storeEquals = (this.store == null && other.store == null)
          || (this.store != null && this.store.equals(other.store));
        return this.amount == other.amount && currencyCodeEquals && storeEquals;
    }

    // other methods
}

À première vue, la classeVoucher et son remplacement pourequals() semblent être corrects. Et les deux méthodesequals() se comportent correctement tant que nous comparonsMoney àMoney ouVoucher àVoucher. But what happens, if we compare these two objects?

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 enfreint les critères de symétrie du contratequals().

2.4. Correction de la symétrieequals() avec composition

Pour éviter cet écueil, nous devrions favor composition over inheritance.

Au lieu de sous-classerMoney, créons une classeVoucher 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) {
        if (o == this)
            return true;
        if (!(o instanceof Voucher))
            return false;
        Voucher other = (Voucher) o;
        boolean valueEquals = (this.value == null && other.value == null)
          || (this.value != null && this.value.equals(other.value));
        boolean storeEquals = (this.store == null && other.store == null)
          || (this.store != null && this.store.equals(other.store));
        return valueEquals && storeEquals;
    }

    // other methods
}

Et maintenant, les eaux grassesequals fonctionnent symétriquement comme l'exige le contrat.

3. hashCode()

hashCode() renvoie un entier représentant l'instance actuelle de la classe. Nous devrions calculer cette valeur conformément à la définition de l'égalité pour la classe. Ainsi, la méthodeif we override the equals(), nous devons également remplacerhashCode().

Pour plus de détails, consultez nosguide to hashCode().

3.1. ContrathashCode()

Java SE définit également un contrat pour la méthodehashCode(). Un examen approfondi montre à quel point leshashCode() etequals() sont étroitement liés.

Les trois critères du contrat dehashCode() mentionnent d'une certaine manière la méthodeequals():

  • internal consistency: la valeur dehashCode() ne peut changer que si une propriété qui est dansequals() change

  • equals consistency: * les objets qui sont égaux les uns aux autres doivent renvoyer le même hashCode *

  • collisions:unequal objects may have the same hashCode

3.2. Violation de la cohérence dehashCode() etequals()

Le 2ème critère du contrat des méthodes hashCode a une conséquence importante:If we override equals(), we must also override hashCode(). Et c'est de loin la violation la plus répandue concernant les contrats des méthodesequals() ethashCode().

Voyons un tel exemple:

class Team {

    String city;
    String department;

    @Override
    public final boolean equals(Object o) {
        // implementation
    }
}

La classeTeam remplace uniquementequals(), mais elle utilise toujours implicitement l'implémentation par défaut dehashCode() telle que définie dans la classeObject. Et cela renvoie unhashCode() différent pour chaque instance de la classe. This violates the second rule.

Maintenant, si nous créons deux objetsTeam, à la fois avec la ville "New York" et le département "marketing", ils seront égaux,but they will return different hashCodes.

3.3. ToucheHashMap avec unhashCode() incohérent

Mais pourquoi la violation de contrat dans notre classeTeam est-elle un problème? Eh bien, le problème commence lorsque certaines collections basées sur le hachage sont impliquées. Essayons d'utiliser notre classeTeam comme clé d'unHashMap:

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);

Nous nous attendrions à ce quemyTeamLeader renvoie «Anne». But with the current code, it doesn’t.

Si nous voulons utiliser des instances de la classeTeam comme clésHashMap, nous devons redéfinir la méthodehashCode() afin qu'elle adhère au contrat:Equal objects return the same hashCode.

Voyons un exemple de mise en œuvre:

@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;
}

Après ce changement,leaders.get(myTeam) renvoie «Anne» comme prévu.

4. Quand remplaçons-nousequals() ethashCode()?

Generally, we want to override either both of them or neither of them. Nous venons de voir dans la section 3 les conséquences indésirables si nous ignorons cette règle.

La conception par domaine peut nous aider à décider des circonstances dans lesquelles nous devrions les laisser. Pour les classes d'entités - pour les objets ayant une identité intrinsèque - l'implémentation par défaut est souvent utile.

Cependant,for value objects, we usually prefer equality based on their properties. Vous voulez donc remplacerequals() ethashCode(). Souvenez-vous de notre classeMoney de la section 2: 55 USD équivaut à 55 USD, même s’il s’agit de deux instances distinctes.

5. Aides à la mise en œuvre

Nous n'écrivons généralement pas la mise en œuvre de ces méthodes à la main. Comme on peut le voir, il y a pas mal de pièges.

Une manière courante consiste àlet our IDE générer les méthodesequals() ethashCode().

Apache Commons Lang etGoogle Guava ont des classes d'assistance afin de simplifier l'écriture des deux méthodes.

Project Lombok fournit également une annotation@EqualsAndHashCode. Note again how equals() and hashCode() “go together” and even have a common annotation.

6. Vérification des contrats

Si nous voulons vérifier si nos implémentations respectent les contrats Java SE et également certaines bonnes pratiques,we can use the EqualsVerifier library.

Ajoutons la dépendance de test MavenEqualsVerifier:


    nl.jqno.equalsverifier
    equalsverifier
    3.0.3
    test

Vérifions que notre classeTeam suit les contratsequals() ethashCode():

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

Il convient de noter queEqualsVerifier teste à la fois les méthodesequals() ethashCode().

EqualsVerifier is much stricter than the Java SE contract. Par exemple, il s'assure que nos méthodes ne peuvent pas lancer unNullPointerException.. De plus, il impose que les deux méthodes, ou la classe elle-même, soient finales.

Il est important de réaliser quethe default configuration of EqualsVerifier allows only immutable fields. Il s'agit d'un contrôle plus strict que ne le permet le contrat Java SE. Cela adhère à une recommandation de la conception par domaine de rendre les objets de valeur immuables.

Si nous trouvons que certaines des contraintes intégrées sont inutiles, nous pouvons ajouter unsuppress(Warning.SPECIFIC_WARNING) à notre appelEqualsVerifier.

7. Conclusion

Dans cet article, nous avons discuté des contratsequals() ethashCode(). Nous devrions nous rappeler de:

  • Remplacez toujourshashCode() si nous remplaçonsequals()

  • Remplacerequals() ethashCode()  pour les objets valeur

  • Soyez conscient des pièges de l'extension des classes qui ont remplacéequals() ethashCode()

  • Pensez à utiliser un IDE ou une bibliothèque tierce pour générer les méthodesequals() ethashCode()

  • Pensez à utiliser EqualsVerifier pour tester notre implémentation

Enfin, tous les exemples de code peuvent être trouvésover on GitHub.