0Pricing
Java Academy · Leçon

Contrat equals/hashCode

Pourquoi les deux doivent être cohérents

Contrat equals/hashCode est une leçon Java Academy gratuite sur CoddyKit. Ceci est la leçon 2 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Java Academy, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Java Academy comprend 4 leçons au total.

Deux méthodes, un seul contrat

Chaque objet Java hérite de equals() et de hashCode() depuis Object.

Lorsque vous redéfinissez l’une de ces méthodes, vous devez presque toujours redéfinir l’autre. Elles forment un contrat contraignant sur lequel reposent les collections fondées sur le hachage.

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

La règle fondamentale

Le contrat stipule ce qui suit :

  • Si a.equals(b) est vrai, alors a.hashCode() doit être égal à b.hashCode().
  • Si les codes de hachage sont différents, les objets sont nécessairement différents.

Des objets égaux doivent avoir des codes de hachage égaux. La réciproque n’est pas obligatoire.

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

Des objets différents peuvent partager un hash

Deux objets différents peuvent tout à fait partager un hash. Il s’agit simplement d’une collision, que la map gère avec equals().

La seule situation interdite est donc la suivante : des objets égaux avec des codes de hachage différents.

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

Une classe qui ne redéfinit que equals

Voici une classe défectueuse. Elle redéfinit equals(), mais pas hashCode().

Deux points logiquement égaux ont alors des codes de hachage différents, hérités de la classe racine, ce qui viole le contrat.

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

Le bogue dans un HashSet

La classe défectueuse échoue dans un HashSet. L’ensemble cherche dans le compartiment choisi par le mauvais hash et ne trouve jamais l’élément égal.

Vous obtenez alors des doublons qui auraient dû être rejetés.

import java.util.HashSet;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

La correction

Redéfinissez les deux méthodes et dérivez-les des mêmes champs. Les objets égaux partagent alors un hash et se retrouvent dans le même compartiment.

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals doit être réflexive

Un equals() correct respecte quatre propriétés. La première est la réflexivité : x.equals(x) doit être vrai.

Cela semble évident, mais une logique personnalisée peut rompre cette propriété si vous oubliez de gérer le cas où il s’agit de la même référence.

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

Symétrique et transitive

Deux autres règles :

  • Symétrique : si a.equals(b), alors b.equals(a).
  • Transitive : si a.equals(b) et b.equals(c), alors a.equals(c).

Mélanger des types au sein d’une hiérarchie de classes rompt souvent la symétrie ; comparez donc soigneusement les classes.

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

Cohérente

La quatrième règle est la cohérence : des appels répétés renvoient le même résultat tant que les objets ne changent pas.

C’est pourquoi vous devriez fonder equals et hashCode sur des champs immuables. Modifier après l’insertion un champ utilisé par hashCode corrompt la map.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

Les enregistrements le font pour vous

Un record Java génère automatiquement equals() et hashCode() à partir de tous ses composants.

Pour des données semblables à des valeurs, privilégiez les enregistrements. Le contrat est respecté gratuitement et reste synchronisé.

import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

Le piège des clés mutables

Si vous modifiez après l’insertion dans une map un champ utilisé par hashCode, vous ne pouvez plus retrouver l’objet.

La map cherche dans l’ancien compartiment ; l’objet est maintenant haché vers un nouveau. L’entrée devient un fantôme perdu.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

Vérification rapide

Vérifiez votre compréhension du contrat.

Récapitulatif

Vous avez appris le contrat equals/hashCode :

  • Des objets égaux doivent avoir des codes de hachage égaux.
  • equals doit être réflexive, symétrique, transitive et cohérente.
  • Fondez les deux méthodes sur les mêmes champs immuables.
  • Les enregistrements génèrent gratuitement des implémentations correctes.

Ensuite, vous écrirez vous-même un hashCode correct.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}

Questions Fréquemment Posées

La leçon « Contrat equals/hashCode » est-elle gratuite ?

Oui — le texte complet de « Contrat equals/hashCode » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Java Academy, passe à CoddyKit PRO. Le cours Java Academy comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Contrat equals/hashCode » ?

Pourquoi les deux doivent être cohérents Tu pratiques Java Academy avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Java Academy ?

Aucune expérience préalable n'est requise. Java Academy sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 2 sur 4.

Combien de temps prend la leçon « Contrat equals/hashCode » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Java Academy ?

Oui. Chaque leçon Java Academy inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Fonctionnement de HashMap
  2. Contrat equals/hashCode
  3. Implémenter hashCode
  4. Transformation en arbre et performances
← Retour à Java Academy