Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter

Support de cours · chapitre 11

Le contrat d'Object

Les méthodes que toute classe hérite d'Object et qu'il faut souvent redéfinir, le contrat qui lie equals et hashCode, et ce qui se passe quand il n'est pas respecté.

Ce chapitre est une révision de la COOA : sa notion (N4) est vérifiée par le diagnostic du 21/10, et n'est pas à l'épreuve.

Toute classe Java hérite implicitement de java.lang.Object :

class CanardEnPlastique { }
// est exactement
class CanardEnPlastique extends Object { }

Elle hérite donc de toString(), equals(Object), hashCode(), clone() et getClass(). Les implémentations par défaut sont correctes, mais elles correspondent rarement à ce que l'on souhaite.

toString()

Par défaut, toString() renvoie le nom de la classe suivi d'une adresse en hexadécimal.

CanardEnPlastique c = new CanardEnPlastique("Gérard", 499);
System.out.println(c);        // CanardEnPlastique@6d06d69c

Ce résultat n'apporte rien dans un journal, dans un débogueur ou dans un message d'erreur. On redéfinit donc la méthode :

@Override
public String toString() {
    return "CanardEnPlastique[nom=" + nom + ", prix=" + prix + "]";
}

== et equals

CanardEnPlastique a = new CanardEnPlastique("Jacques", 399);
CanardEnPlastique b = new CanardEnPlastique("Jacques", 399);

a == b          // false : deux objets distincts en mémoire
a.equals(b)     // false aussi, tant qu'equals n'est pas redéfini

== compare les références : il indique s'il s'agit du même objet en mémoire. equals compare ce que vous choisissez de comparer : il indique si les deux canards sont égaux au sens métier.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    CanardEnPlastique autre = (CanardEnPlastique) o;
    return prix == autre.prix && nom.equals(autre.nom);
}

Un equals correct est réflexif (a.equals(a)), symétrique (si a.equals(b) alors b.equals(a)), transitif, cohérent dans le temps, et renvoie false pour null.

Le contrat avec hashCode

Si l'on redéfinit equals sans redéfinir hashCode, les collections de hachage ne fonctionnent plus correctement, comme le montre l'exemple suivant.

Avant de lancer le programme, essayez de prévoir combien de canards contient chacune des trois bassines.

Bassine.java

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

/** Ni equals ni hashCode : deux canards identiques restent deux objets. */
class CanardBrut {
    final String nom;
    CanardBrut(String nom) { this.nom = nom; }
}

/** equals seul : le piège. */
class CanardBancal {
    final String nom;
    CanardBancal(String nom) { this.nom = nom; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        return nom.equals(((CanardBancal) o).nom);
    }
    // hashCode() n'est PAS redéfini
}

/** Les deux, cohérents. */
class CanardCorrect {
    final String nom;
    CanardCorrect(String nom) { this.nom = nom; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        return nom.equals(((CanardCorrect) o).nom);
    }

    @Override public int hashCode() { return nom.hashCode(); }
}

class Demo {
    public static void main(String[] args) {

        Set<CanardBrut> a = new HashSet<CanardBrut>();
        a.add(new CanardBrut("Jacques"));
        a.add(new CanardBrut("Jacques"));
        System.out.println("1. sans rien       : " + a.size() + " canard(s)");

        CanardBancal j1 = new CanardBancal("Jacques"), j2 = new CanardBancal("Jacques");
        Set<CanardBancal> b = new HashSet<CanardBancal>();
        b.add(j1);
        b.add(j2);
        System.out.println("2. equals seul     : " + b.size() + " canard(s)"
                         + "   alors que j1.equals(j2) = " + j1.equals(j2));
        System.out.println("   contains(j2)    : " + b.contains(j2)
                         + "   (il y est, mais on ne le retrouve pas toujours)");

        Set<CanardCorrect> c = new HashSet<CanardCorrect>();
        c.add(new CanardCorrect("Jacques"));
        c.add(new CanardCorrect("Jacques"));
        System.out.println("3. equals+hashCode : " + c.size() + " canard(s)");
    }
}

Ici, un atelier : on y écrit et on y exécute du code. Il demande JavaScript.

Le même contrat s'applique à HashMap, HashSet et LinkedHashMap, et aux méthodes contains, remove et get.

clone() et la copie superficielle

CanardCompose original = new CanardCompose("Jean", 799, new Accessoire("chapeau", "rouge"));
CanardCompose copie = (CanardCompose) original.clone();

original.getAccessoire() == copie.getAccessoire();   // true : le MÊME accessoire

Le clone() par défaut copie les champs un par un. Pour un champ qui contient une référence, c'est la référence qui est copiée : les deux canards partagent le même accessoire, et une modification faite par l'un est visible par l'autre.

Une copie profonde recrée les objets qui doivent être propres à chaque copie :

@Override
public CanardCompose clone() {
    Accessoire copieAcc = new Accessoire(accessoire.getType(), accessoire.getCouleur());
    return new CanardCompose(getNom(), getPrix(), copieAcc);
}

On retrouve la question du chapitre 3 : composition ou agrégation ? Une aile doit être copiée, alors qu'un dresseur peut être partagé. clone() ne peut pas le deviner : c'est à vous de le décider.

Question d'épreuve

Que se passe-t-il si l'on redéfinit equals sans redéfinir hashCode ?

Les collections de hachage ne fonctionnent plus correctement : deux objets égaux ont des empreintes différentes et sont rangés dans des seaux différents, si bien qu'un HashSet accepte des doublons qu'equals déclare identiques. Le résultat de contains et de remove devient imprévisible.