web-dev-qa-db-fra.com

Comment un objet Java HashMap gère-t-il différents objets avec le même code de hachage?

Selon ma compréhension, je pense:

  1. Il est parfaitement légal que deux objets aient le même hashcode.
  2. Si deux objets sont égaux (en utilisant la méthode equals ()), ils ont le même hashcode.
  3. Si deux objets ne sont pas égaux, ils ne peuvent pas avoir le même hashcode

Ai-je raison?

Maintenant, si je suis correct, j'ai la question suivante: La HashMap utilise en interne le hashcode de l'objet. Donc, si deux objets peuvent avoir le même hashcode, comment la HashMap peut-elle suivre quelle clé elle utilise?

Quelqu'un peut-il expliquer comment la variable HashMap utilise en interne le hashcode de l'objet?

188
akshay

Un hashmap fonctionne comme ceci (ceci est un peu simplifié, mais il illustre le mécanisme de base):

Il utilise un certain nombre de "compartiments" pour stocker les paires clé-valeur. Chaque compartiment a un numéro unique - c'est ce qui l'identifie. Lorsque vous insérez une paire clé-valeur dans la carte, la table de hachage examine le code de hachage de la clé et stocke la paire dans le compartiment dont l'identificateur est le code de hachage de la clé. Par exemple: Le code de hachage de la clé est 235 -> la paire est stockée dans le compartiment numéro 235. (Notez qu'un compartiment peut stocker plus d'une paire clé-valeur).

Lorsque vous recherchez une valeur dans la table de hachage, en lui attribuant une clé, il examinera d'abord le code de hachage de la clé que vous avez donnée. La table de hachage examinera ensuite le compartiment correspondant, puis comparera la clé que vous avez donnée avec les clés de toutes les paires du compartiment, en les comparant à equals().

Vous pouvez maintenant voir à quel point cela est très efficace pour rechercher des paires clé-valeur dans une carte: grâce au code de hachage de la clé, la table de hachage sait immédiatement dans quel compartiment il doit regarder, de sorte qu'il ne doit se confronter qu'à ce qu'il contient.

En examinant le mécanisme ci-dessus, vous pouvez également voir quelles conditions sont nécessaires pour les méthodes de touches hashCode() et equals():

  • Si deux clés sont identiques (equals() renvoie true lorsque vous les comparez), leur méthode hashCode() doit renvoyer le même nombre. Si les clés violent cela, les clés égales peuvent être stockées dans des compartiments différents et la table de hachage ne pourra pas trouver les paires clé-valeur (car elle va rechercher dans le même compartiment).

  • Si deux clés sont différentes, peu importe si leur code de hachage est identique ou non. Ils seront stockés dans le même compartiment si leurs codes de hachage sont identiques, et dans ce cas, la table de hachage utilisera equals() pour les différencier.

308
Jesper

Votre troisième affirmation est incorrecte.

Il est parfaitement légal que deux objets inégaux aient le même code de hachage. HashMap l'utilise comme "filtre de premier passage" afin que la carte puisse trouver rapidement les entrées possible avec la clé spécifiée. Les clés avec le même code de hachage sont ensuite testées pour vérifier leur égalité avec la clé spécifiée.

Vous ne voudriez pas que deux objets inégaux ne puissent pas avoir le même code de hachage, sinon cela vous limiterait à 232 objets possibles. (Cela signifierait également que différents types ne pourraient même pas utiliser les champs d'un objet pour générer des codes de hachage, car d'autres classes pourraient générer le même hachage.)

83
Jon Skeet

HashMap structure diagram

HashMap est un tableau d'objets Entry.

Considérez HashMap comme un simple tableau d'objets.

Jetez un coup d'œil à ce qu'est cette Object:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;
… 
}

Chaque objet Entry représente une paire clé-valeur. Le champ next fait référence à un autre objet Entry si un compartiment a plusieurs Entry.

Parfois, il peut arriver que les codes de hachage de 2 objets différents soient identiques. Dans ce cas, deux objets seront enregistrés dans un compartiment et présentés sous forme de liste chaînée. Le point d’entrée est l’objet ajouté le plus récemment. Cet objet fait référence à un autre objet avec le champ next et ainsi de suite. La dernière entrée fait référence à null.

Lorsque vous créez une HashMap avec le constructeur par défaut

HashMap hashMap = new HashMap();

Le tableau est créé avec une taille de charge de 16 et un solde de charge de 0,75.

Ajouter une nouvelle paire clé-valeur

  1. Calculer le hashcode pour la clé
  2. Calculer la position hash % (arrayLength-1) où l’élément doit être placé (numéro de compartiment)
  3. Si vous essayez d'ajouter une valeur avec une clé qui a déjà été enregistrée dans HashMap, la valeur est écrasée.
  4. Sinon, l'élément est ajouté au compartiment.

Si le compartiment contient déjà au moins un élément, un nouvel élément est ajouté et placé à la première position du compartiment. Son champ next fait référence à l'ancien élément.

Effacement

  1. Calculer le hashcode pour la clé donnée
  2. Calculer le numéro de compartiment hash % (arrayLength-1)
  3. Obtenez une référence au premier objet Entry dans le compartiment et, au moyen de la méthode equals, parcourez toutes les entrées du compartiment donné. Finalement, nous trouverons la bonne Entry. Si un élément souhaité n’est pas trouvé, retourne null 
61
Sergii Shevchyk

Vous pouvez trouver d’excellentes informations sur http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-Java.html

Résumer: 

HashMap fonctionne sur le principe de hachage

put (clé, valeur): HashMap stocke la clé et l'objet valeur sous la forme Map.Entry. Hashmap applique le hashcode (clé) pour obtenir le compartiment. en cas de collision, HashMap utilise LinkedList pour stocker un objet. 

get (key): HashMap utilise le hashcode de Key Object pour rechercher l'emplacement du compartiment, puis appelle la méthode keys.equals () pour identifier le noeud correct dans LinkedList et renvoie l'objet de valeur associé à cette clé dans Java HashMap.

34
Abhijit Gaikwad

Le hashcode détermine le compartiment à vérifier par la hashmap. S'il y a plus d'un objet dans le compartiment, une recherche linéaire est effectuée pour déterminer quel élément du compartiment est égal à la méthode souhaitée (à l'aide de la méthode equals()).

En d’autres termes, si vous avez un hashcode parfait, alors que l’accès à hashmap est constant, vous n’aurez plus à itérer dans un compartiment (techniquement, vous devez également disposer de compartiments MAX_INT, l’implémentation Java peut partager plusieurs codes de hachage dans le même compartiment). réduire les besoins en espace). Si vous avez le pire hashcode (retourne toujours le même nombre), votre accès à la hashmap devient linéaire puisque vous devez rechercher dans chaque élément de la carte (ils sont tous dans le même compartiment) pour obtenir ce que vous voulez.

La plupart du temps, un hashcode bien écrit n'est pas parfait mais suffisamment unique pour vous donner un accès plus ou moins constant.

14
Pace

Vous vous trompez sur le point trois. Deux entrées peuvent avoir le même code de hachage mais ne pas être égales. Regardez l'implémentation de HashMap.get depuis OpenJdk . Vous pouvez voir qu'il vérifie que les hachages sont égaux et les clés sont égales. Si le point trois était vrai, il serait alors inutile de vérifier que les clés sont égales. Le code de hachage est comparé avant la clé car le premier est une comparaison plus efficace. 

Si vous souhaitez en savoir un peu plus à ce sujet, consultez l'article de Wikipedia sur Résolution de collision Open Addressing , qui, selon moi, est le mécanisme utilisé par la mise en oeuvre d'OpenJdk. Ce mécanisme est légèrement différent de l’approche dite du "seau", comme l’indique une autre réponse. 

11
Leif Wickland
import Java.util.HashMap;

public class Students  {
    String name;
    int age;

    Students(String name, int age ){
        this.name = name;
        this.age=age;
    }

    @Override
    public int hashCode() {
        System.out.println("__hash__");
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        System.out.println("__eq__");
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Students other = (Students) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    public static void main(String[] args) {

        Students S1 = new Students("taj",22);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Output:

__ hash __

116232

__ hash __

116201

__ hash __

__ hash __

2

Nous voyons donc ici que si les objets S1 et S2 ont un contenu différent, nous sommes pratiquement certains que notre méthode Hashcode surchargée générera un Hashcode différent (116232,11601) pour les deux objets. NOW puisqu'il existe différents codes de hachage, il ne sera même pas utile d'appeler la méthode EQUALS. Parce qu'un code de hachage différent GARANTIT un contenu différent dans un objet.

    public static void main(String[] args) {

        Students S1 = new Students("taj",21);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Now lets change out main method a little bit. Output after this change is 

__ hash __

116201

__ hash __

116201

__ hash __

__ hash __

__ eq __

1
We can clearly see that equal method is called. Here is print statement __eq__, since we have same hashcode, then content of objects MAY or MAY not be similar. So program internally  calls Equal method to verify this. 


Conclusion 
If hashcode is different , equal method will not get called. 
if hashcode is same, equal method will get called.

Thanks , hope it helps. 
5
Tajinder Singh

Chaque objet Entry représente une paire clé-valeur. Le champ suivant fait référence à un autre objet Entrée si un compartiment a plus d'une entrée.

Parfois, il peut arriver que hashCodes pour 2 objets différents soient identiques. Dans ce cas, 2 objets seront enregistrés dans un compartiment et seront présentés sous forme de liste liée. Le point d'entrée est un objet ajouté plus récemment. Cet objet fait référence à un autre objet avec le champ suivant et ainsi de suite. La dernière entrée fait référence à null . Lorsque vous créez HashMap avec le constructeur par défaut

Le tableau est créé avec une taille 16 et un équilibre de charge de 0,75 par défaut.

enter image description here

(La source)

2
Premraj

La carte de hachage fonctionne sur le principe de hachage 

HashMap get (Key k), la méthode appelle la méthode hashCode sur l'objet key et applique hashValue renvoyée à sa propre fonction de hachage statique pour rechercher un emplacement de compartiment (tableau de sauvegarde) dans lequel les clés et les valeurs sont stockées sous la forme d'une classe imbriquée appelée Entry (Map. Entrée). Vous avez donc conclu que, à partir de la ligne précédente, la clé et la valeur sont stockées dans le compartiment sous forme d'objet Entry. Donc, penser que Seule la valeur est stockée dans le seau n'est pas correct et ne donnera pas une bonne impression à l'intervieweur.

  • Chaque fois que nous appelons la méthode get (Key k) sur l'objet HashMap. Tout d'abord, il vérifie que la clé est nulle ou non. Notez qu'il ne peut y avoir qu'une seule clé NULL dans HashMap. 

Si key est null, les clés Null sont toujours mappées sur le hachage 0, donc l'index 0.

Si key n'est pas nul, il appellera hashfunction sur l'objet key, voir la ligne 4 de la méthode ci-dessus, key.hashCode (). Ainsi, après que key.hashCode () retourne hashValue, la ligne 4 ressemble à

            int hash = hash(hashValue)

et maintenant, il retourne hashValue renvoyé dans sa propre fonction de hachage.

On peut se demander pourquoi nous calculons encore la valeur de hachage en utilisant hash (hashValue). La réponse est qu'il défend contre les fonctions de hachage de mauvaise qualité.

Désormais, la valeur de hachage finale est utilisée pour trouver l'emplacement du compartiment dans lequel l'objet Entrée est stocké. Les objets d’entrée sont stockés dans le compartiment comme ceci (hachage, clé, valeur, bucketindex) 

1
user2706780

Je n'entrerai pas dans les détails du fonctionnement de HashMap, mais donnerai un exemple afin que nous puissions nous rappeler comment fonctionne HashMap en le rapportant à la réalité.

Nous avons Key, Value, HashCode et un seau.

Pendant un certain temps, nous allons relier chacune d’elles avec ce qui suit:

  • Seau -> Une société
  • HashCode -> Adresse de la société (toujours unique)
  • Valeur -> Une maison dans la société
  • Clé -> Adresse de la maison.

Utilisation de Map.get (clé):

Stevie veut se rendre chez son ami (Josse) qui habite une villa dans une société VIP, qu’il s’agisse de la société JavaLovers. L'adresse de Josse est son SSN (qui est différent pour tout le monde) . Il existe un index dans lequel nous trouvons le nom de la société basé sur le SSN . Cet index peut être considéré comme un algorithme le HashCode.

  • Nom de la société SSN
  • 92313 (Josse's) - JavaLovers
  • 13214 - AngularJSLovers
  • 98080 - JavaLovers
  • 53808 - amoureux de la biologie

  1. Ce SSN (clé) nous donne d’abord un HashCode (de la table d’index) qui n’est autre que le nom de la société.
  2. Maintenant, les maisons multiples peuvent être dans la même société, ainsi le HashCode peut être commun.
  3. Supposons que la Société soit commune à deux maisons, comment allons-nous identifier quelle maison nous allons, oui, en utilisant la clé (SSN) qui n’est autre que l’adresse de la maison

Utilisation de Map.put (clé, valeur)

Ceci trouve une société appropriée pour cette valeur en trouvant le HashCode, puis la valeur est stockée.

J'espère que cela aide et cela est ouvert pour des modifications.

1
Prashant K

deux objets sont égaux, implique qu'ils ont le même hashcode, mais pas l'inverse

Mise à jour de Java 8 dans HashMap -

vous faites cette opération dans votre code - 

myHashmap.put("old","key-value-pair");
myHashMap.put("very-old","old-key-value-pair");

alors supposons que votre hashcode retourné pour les deux clés "old" et "very-old" soit identique. Alors qu'est-ce qui va arriver.

myHashMap est un HashMap, et supposons qu'au départ vous ne spécifiez pas sa capacité. La capacité par défaut selon Java est donc de 16. Ainsi, dès que vous avez initialisé hashmap avec le nouveau mot-clé, 16 compartiments ont été créés. maintenant, quand vous avez exécuté la première déclaration-

myHashmap.put("old","key-value-pair");

alors le hashcode pour "old" est calculé, et comme le hashcode peut aussi être un très grand entier, Java le fait en interne - (le hash est hashcode ici et >>> est à droite)

hash XOR hash >>> 16

alors, pour donner une image plus grande, il retournera un index, qui serait compris entre 0 et 15. Maintenant, votre paire valeur-clé "old" et "key-value-pair" sera convertie en variable d'instance clé et valeur de l'objet Entry. et ensuite cet objet d'entrée sera stocké dans le compartiment, ou vous pouvez dire que, à un index particulier, cet objet d'entrée sera stocké.

FYI- Entry est une classe dans Map Interface- Map.Entry, avec ces signature/définition

class Entry{
          final Key k;
          value v;
          final int hash;
          Entry next;
}

maintenant, quand vous exécuterez la prochaine déclaration - 

myHashmap.put("very-old","old-key-value-pair");

et "very-old" donne le même hashcode que "old", de sorte que cette nouvelle paire clé-valeur est à nouveau envoyée au même index ou au même compartiment. Mais comme ce compartiment n'est pas vide, la variable next de l'objet Entry est utilisée pour stocker cette nouvelle paire de valeurs de clé.

et cela sera stocké en tant que liste chaînée pour chaque objet ayant le même hashcode, mais un TRIEFY_THRESHOLD est spécifié avec la valeur 6. Ainsi, après cela, la liste chaînée est convertie en arbre équilibré (arbre rouge-noir) avec le premier élément en tant que racine.

1
anubhs

Comme on dit, une image vaut 1000 mots. Je dis: un code vaut mieux que 1000 mots. Voici le code source de HashMap. Obtenir la méthode:

/**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

Il devient donc clair que le hachage est utilisé pour trouver le "compartiment" et que le premier élément est toujours vérifié dans ce compartiment. Sinon, equals de la clé est utilisé pour trouver l'élément réel dans la liste liée.

Voyons la méthode put():

  /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

C'est un peu plus compliqué, mais il devient clair que le nouvel élément est placé dans l'onglet à la position calculée en fonction du hachage:

i = (n - 1) & hash here i est l'index où le nouvel élément sera placé (ou il s'agit du "compartiment"). n est la taille du tableau tab (tableau de "compartiments").

Premièrement, on essaie de le placer comme premier élément de ce "seau". S'il existe déjà un élément, ajoutez un nouveau nœud à la liste.

0
ACV