web-dev-qa-db-fra.com

Comprendre le fonctionnement d'égaux et de hashCode dans une carte de hachage

J'ai ce code de test:

import Java.util.*;

class MapEQ {

  public static void main(String[] args) {
   Map<ToDos, String> m = new HashMap<ToDos, String>();
   ToDos t1 = new ToDos("Monday");
   ToDos t2 = new ToDos("Monday");
   ToDos t3 = new ToDos("Tuesday");
   m.put(t1, "doLaundry");
   m.put(t2, "payBills");
   m.put(t3, "cleanAttic");
   System.out.println(m.size());
} }

class ToDos{

  String day;

  ToDos(String d) { day = d; }

  public boolean equals(Object o) {
      return ((ToDos)o).day == this.day;
 }

// public int hashCode() { return 9; }
}

Lorsque // public int hashCode() { return 9; } n'est pas commenté m.size() renvoie 2, lorsqu'il est laissé commenté, il renvoie trois. Pourquoi?

36
andandandand

HashMap utilise hashCode(), == et equals() pour la recherche d'entrée. La séquence de recherche pour une clé donnée k est la suivante:

  • Utilisez k.hashCode() pour déterminer le compartiment dans lequel l'entrée est stockée, le cas échéant.
  • Si trouvé, pour la clé k1 de chaque entrée dans ce compartiment, si k == k1 || k.equals(k1), alors retourne l'entrée de k1
  • Tout autre résultat, aucune entrée correspondante

Pour illustrer votre propos à l'aide d'un exemple, supposons que nous voulions créer une variable HashMap où les clés sont quelque chose qui est «logiquement équivalent» si elles ont la même valeur entière, représentée par la classe AmbiguousInteger. Nous construisons ensuite une HashMap, mettons une entrée, puis essayons de remplacer sa valeur et de récupérer valeur par clé.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

HashMap<AmbiguousInteger, Integer> map = new HashMap<>();
// logically equivalent keys
AmbiguousInteger key1 = new AmbiguousInteger(1),
                 key2 = new AmbiguousInteger(1),
                 key3 = new AmbiguousInteger(1);
map.put(key1, 1); // put in value for entry '1'
map.put(key2, 2); // attempt to override value for entry '1'
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(key3));

Expected: 2, 2, 2

Ne pas écraser hashCode() et equals(): par défaut, Java génère différentes valeurs hashCode() pour différents objets. HashMap utilise ces valeurs pour mapper key1 et key2 dans différents compartiments. key3 n'a pas de compartiment correspondant, il n'a donc aucune valeur.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Output:   1, 2, null

Override hashCode() uniquement:HashMap mappe key1 et key2 dans le même compartiment, mais ils restent des entrées différentes en raison des vérifications key1 == key2 et key1.equals(key2), comme par défaut equals() utilise ==. vérifier, et ils se réfèrent à différentes instances. key3 échoue à la fois == et equals() par rapport à key1 et key2 et n'a donc aucune valeur correspondante.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Output:   1, 2, null

Override equals() uniquement:HashMap mappe toutes les clés dans des compartiments différents en raison de l'option hashCode() différente. == ou equals() check est sans importance ici car HashMap n'atteint jamais le point où il doit être utilisé.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Actual:   1, 2, null

Remplacez les deux fonctions hashCode() et equals(): HashMap mappe key1, key2 et key3 dans le même compartiment. Les vérifications == échouent lors de la comparaison d'instances différentes, mais les vérifications equals() réussissent car elles ont toutes la même valeur et sont considérées comme "logiquement équivalentes" par notre logique.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return value;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

Que se passe-t-il si hashCode() est aléatoire?: HashMap affectera un compartiment différent pour chaque opération. Vous ne retrouverez donc jamais la même entrée que celle que vous avez précédemment insérée.

class AmbiguousInteger {
    private static int staticInt;
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return ++staticInt; // every subsequent call gets different value
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to no bucket, no corresponding value
map.get(key2); // map to no bucket, no corresponding value
map.get(key3); // map to no bucket, no corresponding value
Expected: 2, 2, 2
Actual:   null, null, null

Que se passe-t-il si hashCode() est toujours le même?: HashMap mappe toutes les clés dans un grand compartiment. Dans ce cas, votre code est fonctionnellement correct, mais l'utilisation de HashMap est pratiquement redondante, car toute extraction aurait besoin de parcourir toutes les entrées de ce compartiment unique dans le temps O(N) ( ou O(logN) pour Java 8 ), équivalent à l'utilisation de List.

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

Et si equals est toujours faux?: == check réussit lorsque nous comparons la même instance à elle-même, mais échoue sinon, equals check échoue toujours pour que key1, key2 et key3 soient considérés comme 'logiquement différents' et mappés vers des entrées différentes même seau en raison de même hashCode().

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return false;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Actual:   1, 2, null

Okay si equals est toujours vrai maintenant?: vous dites fondamentalement que tous les objets sont considérés comme «logiquement équivalents» à un autre, ils sont donc tous mappés vers le même compartiment (en raison de la même entrée hashCode()).

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.put(new AmbiguousInteger(100), 100); // map to bucket 1, set as entry1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   100, 100, 100
47
hidro

Vous avez surchargé equals sans remplacer hashCode. Vous devez vous assurer que, dans tous les cas où equals renvoie true pour deux objets, hashCode renvoie la même valeur. Le code de hachage est un code qui doit être égal si deux objets sont égaux (l'inverse n'est pas nécessairement vrai). Lorsque vous mettez votre valeur codée en dur de 9 po, vous remplissez à nouveau le contrat.

Dans votre carte de hachage, l'égalité n'est testée que dans un compartiment de hachage. Vos deux objets Monday doivent être égaux, mais comme ils renvoient des codes de hachage différents, la méthode equals n'est même pas appelée pour déterminer leur égalité - ils sont placés directement dans des compartiments différents et la possibilité qu'ils soient égaux n'est même pas prise en compte .

44
David M

Je ne saurais trop insister sur le fait que vous devriez lire Chapitre 3 in Effective Java (attention: lien pdf). Dans ce chapitre, vous apprendrez tout ce que vous devez savoir sur les méthodes prioritaires dans Object, et en particulier sur le contrat equals. Josh Bloch a une excellente recette pour remplacer la méthode equals que vous devriez suivre. Et cela vous aidera à comprendre pourquoi vous devriez utiliser equals et non == dans votre implémentation particulière de la méthode equals.

J'espère que cela t'aides. LISEZ-LE S'IL VOUS PLAÎT. (Au moins les deux premiers articles ... et ensuite vous voudrez lire le reste :-).

-À M

8
Tom

Si vous ne substituez pas la méthode hashCode (), votre classe ToDos hérite de la méthode hashCode () par défaut de Object, qui attribue à chaque objet un code de hachage distinct. Cela signifie que t1 et t2 ont deux codes de hachage différents, même si vous les compariez, ils seraient égaux. En fonction de l'implémentation de hashmap particulière, la carte est libre de les stocker séparément (et c'est ce qui se passe réellement).

Lorsque vous substituez correctement la méthode hashCode () pour vous assurer que les objets identiques obtiennent les mêmes codes de hachage, la table de hachage est capable de trouver les deux objets égaux et de les placer dans le même compartiment de hachage.

Une meilleure implémentation donnerait des objets qui sont pas égaux différents codes de hachage, comme ceci:

public int hashCode() {
    return (day != null) ? day.hashCode() : 0;
}
6
Avi

lorsque vous commentez, il retourne 3; 

parce que hashCode () hérité de Object est appelé UNIQUEMENT, ce qui retourne 3 codes de hachage différents pour les 3 objets ToDos. Les hashcodes inégaux signifient que les 3 objets sont destinés à des compartiments différents et equals () renvoie false car ils sont le premier entrant dans leurs compartiments respectifs . Si les hashCodes sont différents, il est entendu à l’avance que les objets sont inégaux ..__ Ils iront dans des seaux différents.

lorsque vous décommentez, il retourne 2; 

parce qu'ici on appelle hashCode () surchargé, ce qui retourne la même valeur pour tous les ToDos et ils devront tous aller dans un compartiment, connectés linéairement . Des hashcodes égaux ne promettent rien sur l'égalité ou l'inégalité des objets. 

hashCode () pour t3 est égal à 9 et comme il s'agit du premier entrant, equals () est false et t3 inséré dans le seau bucket_bucket0.

Ensuite, t2 obtenant le même hashCode () que 9 est destiné au même compartiment0, un traitement ultérieur égal à () sur le résidant t3 déjà présent dans le regroupement0 renvoie false par la définition de equalidden égal ().

Maintenant, t1 avec hashCode () en tant que 9 est également destiné à bucket0, et un appel ultérieur de equals () renvoie true par rapport au t2 préexistant dans le même compartiment. t1 ne parvient pas à entrer dans la carte . La taille nette de la carte est donc 2 -> {ToDos @ 9 = cleanAttic, ToDos @ 9 = payBills}

Ceci explique l'importance de l'implémentation de equals () et de hashCode (), et de telle sorte que les champs utilisés pour déterminer equals () doivent également être pris en compte lors de la détermination de hashCode (). Cela garantira que si deux objets sont égaux, ils auront toujours les mêmes hashCodes. hashCodes ne doit pas être perçu comme un nombre pseudo-aléatoire, car il doit être cohérent avec equals ()

4
Shadab Khan

Selon Effective Java,

Toujours écraser hashCode () lorsque vous écrasez equals ()

pourquoi? Simple, car différents objets (le contenu, pas les références) doivent obtenir des codes de hachage différents; Par contre, des objets identiques devraient avoir le même code de hachage.

Selon ce qui précède, les structures de données associatives Java comparent les résultats obtenus par les invocations equals () et hashCode () pour créer les compartiments. Si les deux sont identiques, les objets sont égaux; sinon non.

Dans le cas spécifique (c'est-à-dire celui présenté ci-dessus), lorsque hashCode () est commenté , un nombre aléatoire est généré pour chaque instance (comportement hérité par Object) sous forme de hachage, le contrôle equals () vérifie les références de String (rappelez-vous Java). String Pool), donc equals () doit renvoyer true mais hashCode () non, le résultat est 3 objets différents stockés. Voyons ce qui se passe si hashCode () respecte le contrat mais renvoie toujours 9 est non commenté . HashCode () est toujours le même, equals () renvoie true pour les deux chaînes du pool (c'est-à-dire "Monday"), et pour elles le compartiment sera identique, ne donnant que 2 éléments. stockée

Par conséquent, il est absolument nécessaire d'utiliser avec prudence les substitutions de hashCode () et d'equals (), en particulier lorsque les types de données composés sont définis par l'utilisateur et qu'ils sont utilisés avec des structures de données associatives Java.

3
Paolo Maresca

Plutôt que de penser à hashCode en termes de mappage hachage-compartiment, je pense qu'il est plus utile de penser de manière plus abstraite: une observation selon laquelle deux objets ont des codes de hachage différents constitue une observation selon laquelle les objets ne sont pas égaux. En conséquence, une observation selon laquelle aucun des objets d'une collection ne possède un code de hachage particulier constitue une observation selon laquelle aucun des objets d'une collection n'est égal à un objet comportant ce code de hachage. De plus, une observation selon laquelle aucun des objets d'une collection ne possède un code de hachage avec un trait ne constitue une observation qu'aucun d'entre eux n'est égal à un objet qui en possède un.

Les tables de hachage travaillent généralement en définissant une famille de traits, dont exactement un sera applicable au code de hachage de chaque objet (par exemple, "étant congru à 0 mod 47", "étant congru à 1 mod 47", etc.), puis avoir une collection d'objets avec chaque trait. Si on donne ensuite un objet et que l'on peut déterminer quel trait s'applique à celui-ci, on peut savoir qu'il doit faire partie d'un ensemble d'objets contenant ce trait.

Que les tables de hachage utilisent généralement une séquence de compartiments numérotés est un détail d'implémentation; ce qui est essentiel, c’est que le code de hachage d’un objet soit rapidement utilisé pour identifier beaucoup de choses avec lesquelles il ne peut être égal, et avec lesquelles il n’aura donc pas besoin d’être comparé.

0
supercat

Lorsque hashCode n'est pas commenté, HashMap voit t1 et t2 comme étant la même chose; ainsi, la valeur de t2 est supérieure à celle de t1. Pour comprendre comment cela fonctionne, notez que lorsque hashCode renvoie la même chose pour deux instances, elles se retrouvent dans le même compartiment HashMap. Lorsque vous essayez d'insérer une seconde chose dans le même compartiment (dans ce cas, t2 est inséré alors que t1 est déjà présent), HashMap analyse le compartiment pour rechercher une autre clé égale. Dans votre cas, t1 et t2 sont égaux car ils ont le même jour. À ce stade, "payBills" clobbers "doLaundry". Quant à savoir si t2 clobbers t1 en tant que clé, je pense que cela n’est pas défini; ainsi, l'un ou l'autre comportement est autorisé.

Il y a quelques points importants à considérer ici:

  1. Deux instances de tâches sont-elles vraiment identiques simplement parce qu’elles ont le même jour de la semaine?
  2. Chaque fois que vous implémentez equals, vous devez implémenter hashCode afin que deux objets égaux aient également les mêmes valeurs hashCode. Ceci est une hypothèse fondamentale faite par HashMap. Ceci est probablement également vrai pour tout ce qui repose sur la méthode hashCode.
  3. Concevez votre méthode hashCode de sorte que les codes de hachage soient répartis de manière égale. sinon, vous n'obtiendrez pas les avantages de hachage sur les performances. Dans cette perspective, renvoyer 9 est l’une des pires choses que vous puissiez faire.
0
allyourcode

Chaque fois que vous créez un nouvel objet en Java, la machine virtuelle Java lui attribue un code de hachage unique. Si vous ne remplacez pas la méthode hashcode, l'objet obtiendra un hascode unique et donc un compartiment unique (le compartiment Imagine n'est rien d'autre qu'un endroit dans la mémoire où la machine virtuelle Java cherchera un objet).

(vous pouvez vérifier l'unicité d'un hashcode en appelant la méthode hashcode sur chaque objet et en imprimant leurs valeurs sur la console)

Dans votre cas, lorsque vous commentez une méthode hashcode, hashmap commence par rechercher un compartiment ayant le même hashcode que la méthode retourne. Et chaque fois que vous retournez le même hashcode. Désormais, lorsque hashmap trouvera ce compartiment, il comparera l'objet actuel à l'objet résidant dans le compartiment à l'aide de la méthode euqals. Ici, il trouve "lundi" et l'implémentation de hashmap ne permet donc pas de l'ajouter à nouveau car il existe déjà un objet ayant le même hashcode et la même implémentation euqality.

Lorsque vous commentez la méthode hashcode, la machine virtuelle Java renvoie simplement un hashcode différent pour les trois objets. Par conséquent, elle ne se préoccupe jamais des objets de comapring utilisant la méthode equals. Et donc, il y aura trois objets différents dans Map ajoutés par l'implémentation de hashmap.

0
Hiren Savalia