Implémentation HashMap Java 8


92

Selon le document de lien suivant: Implémentation Java HashMap

Je suis confus avec la mise en œuvre de HashMap(ou plutôt, une amélioration dans HashMap). Mes requêtes sont:

d'abord

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

Pourquoi et comment ces constantes sont-elles utilisées? Je veux des exemples clairs pour cela. Comment obtiennent-ils un gain de performance avec cela?

Deuxièmement

Si vous voyez le code source de HashMapdans JDK, vous trouverez la classe interne statique suivante:

static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
    HashMap.TreeNode<K, V> parent;
    HashMap.TreeNode<K, V> left;
    HashMap.TreeNode<K, V> right;
    HashMap.TreeNode<K, V> prev;
    boolean red;

    TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
        super(arg0, arg1, arg2, arg3);
    }

    final HashMap.TreeNode<K, V> root() {
        HashMap.TreeNode arg0 = this;

        while (true) {
            HashMap.TreeNode arg1 = arg0.parent;
            if (arg0.parent == null) {
                return arg0;
            }

            arg0 = arg1;
        }
    }
    //...
}

Comment est-ce utilisé? Je veux juste une explication de l'algorithme .

Réponses:


225

HashMapcontient un certain nombre de seaux. Il utilise hashCodepour déterminer dans quel seau les placer. Par souci de simplicité, imaginez-le comme un module.

Si notre hashcode est 123456 et que nous avons 4 buckets, 123456 % 4 = 0l'élément va donc dans le premier bucket, Bucket 1.

HashMap

Si notre fonction de hashcode est bonne, elle devrait fournir une distribution uniforme afin que tous les compartiments soient utilisés de manière quelque peu égale. Dans ce cas, le compartiment utilise une liste liée pour stocker les valeurs.

Godets liés

Mais vous ne pouvez pas compter sur les gens pour implémenter de bonnes fonctions de hachage. Les gens écriront souvent des fonctions de hachage médiocres, ce qui entraînera une distribution non uniforme. Il est également possible que nous puissions simplement être malchanceux avec nos contributions.

Bad hashmap

Moins cette distribution est uniforme, plus nous nous éloignons des opérations O (1) et plus nous nous rapprochons des opérations O (n).

L'implémentation de Hashmap tente d'atténuer cela en organisant certains buckets en arborescences plutôt qu'en listes liées si les buckets deviennent trop volumineux. C'est à ça que ça TREEIFY_THRESHOLD = 8sert. Si un seau contient plus de huit éléments, il doit devenir un arbre.

Godet d'arbre

Cet arbre est un arbre rouge-noir. Il est d'abord trié par code de hachage. Si les codes de hachage sont identiques, il utilise la compareTométhode de Comparablesi les objets implémentent cette interface, sinon le code de hachage d'identité.

Si des entrées sont supprimées de la carte, le nombre d'entrées dans le compartiment peut être réduit de sorte que cette arborescence n'est plus nécessaire. C'est à cela que UNTREEIFY_THRESHOLD = 6sert. Si le nombre d'éléments dans un bucket tombe en dessous de six, nous pourrions aussi bien revenir à l'utilisation d'une liste chaînée.

Enfin, il y a le MIN_TREEIFY_CAPACITY = 64.

Lorsqu'une carte de hachage augmente en taille, elle se redimensionne automatiquement pour avoir plus de compartiments. Si nous avons une petite carte de hachage, la probabilité que nous obtenions des seaux très pleins est assez élevée, car nous n'avons pas autant de seaux différents dans lesquels placer des éléments. Il est bien préférable d'avoir une carte de hachage plus grande, avec plus de seaux moins pleins. Cette constante dit essentiellement de ne pas commencer à transformer des seaux en arbres si notre carte de hachage est très petite - elle doit d'abord être redimensionnée pour être plus grande.


Pour répondre à votre question sur le gain de performance, ces optimisations ont été ajoutées pour améliorer le pire des cas. Je ne fais que spéculer mais vous ne verriez probablement une amélioration notable des performances à cause de ces optimisations que si votre hashCodefonction n'était pas très bonne.


3
Une distribution non uniforme n'est pas toujours le signe de mauvaises fonctions de hachage. Certains types de données, par exemple String, ont un espace de valeur bien plus grand que le inthashcode, par conséquent, les collisions sont inévitables. Maintenant, cela dépend des valeurs réelles, comme les Strings réels , que vous mettez dans la carte, que vous obteniez une distribution uniforme ou non. Une mauvaise distribution peut être le résultat de la malchance.
Holger

3
+1, j'aimerais ajouter qu'un scénario spécifique que cette approche arborescente atténue est une attaque DOS par collision de hachage . java.lang.Stringa un caractère déterministe et non cryptographique hashCode, de sorte que les attaquants peuvent créer de manière triviale des chaînes distinctes avec des hashCodes en collision. Avant cette optimisation, cela pouvait dégrader les opérations HashMap en temps O (n), maintenant il les dégrade simplement en O (log (n)).
MikeFHay

1
+1, if the objects implement that interface, else the identity hash code.je cherchais cette autre partie.
Numéro945

1
@NateGlenn le code de hachage par défaut si vous ne le remplacez pas
Michael

Je n'ai pas obtenu "Cette constante dit essentiellement de ne pas commencer à créer des seaux dans des arbres si notre carte de hachage est très petite - elle doit d'abord être redimensionnée pour être plus grande." pour MIN_TREEIFY_CAPACITY. Cela signifie-t-il "Une fois que nous insérons une clé qui doit être hachée dans le compartiment contenant déjà 8 TREEIFY_THRESHOLDclés ( ) et s'il y a déjà 64 MIN_TREEIFY_CAPACITYclés ( ) HashMap, la liste liée de ce compartiment est convertie en arbre équilibré."
anir

16

Pour le dire plus simple (autant que je pourrais plus simple) + quelques détails supplémentaires.

Ces propriétés dépendent de beaucoup de choses internes qu'il serait très cool de comprendre - avant de passer directement à elles.

TREEIFY_THRESHOLD -> lorsqu'un seul seau atteint cela (et que le nombre total dépasse MIN_TREEIFY_CAPACITY), il se transforme en un nœud d'arbre rouge / noir parfaitement équilibré . Pourquoi? En raison de la vitesse de recherche. Pensez-y d'une manière différente:

il faudrait au plus 32 étapes pour rechercher une entrée dans un bucket / bin avec des entrées Integer.MAX_VALUE .

Quelques intro pour le sujet suivant. Pourquoi le nombre de bacs / seaux est-il toujours une puissance de deux ? Au moins deux raisons: plus rapide que le fonctionnement modulo et modulo sur les nombres négatifs sera négatif. Et vous ne pouvez pas placer une entrée dans un bucket "négatif":

 int arrayIndex = hashCode % buckets; // will be negative

 buckets[arrayIndex] = Entry; // obviously will fail

Au lieu de cela, il y a une belle astuce utilisée à la place de modulo:

 (n - 1) & hash // n is the number of bins, hash - is the hash function of the key

C'est sémantiquement identique au fonctionnement modulo. Il conservera les bits inférieurs. Cela a une conséquence intéressante lorsque vous faites:

Map<String, String> map = new HashMap<>();

Dans le cas ci-dessus, la décision de l'emplacement d'une entrée est prise en fonction des 4 derniers bits uniquement de votre hashcode.

C'est là que la multiplication des seaux entre en jeu. Dans certaines conditions (cela prendrait beaucoup de temps à expliquer avec précision ), les seaux sont doublés de taille. Pourquoi? Lorsque la taille des godets est doublée, un autre élément entre en jeu .

Donc, vous avez 16 seaux - les 4 derniers bits du hashcode décident où va une entrée. Vous doublez les seaux: 32 seaux - 5 derniers bits décident de la destination de l'entrée.

En tant que tel, ce processus est appelé re-hachage. Cela pourrait devenir lent. C'est (pour les gens qui se soucient) que HashMap est "plaisanté" comme: rapide, rapide, rapide, lent . Il existe d'autres implémentations - recherche hashmap sans pause ...

Désormais, UNTREEIFY_THRESHOLD entre en jeu après un nouveau hachage. À ce stade, certaines entrées peuvent passer de ce bac à d'autres (elles ajoutent un bit de plus au (n-1)&hashcalcul - et en tant que tel peuvent se déplacer vers d' autres buckets) et cela peut atteindre cela UNTREEIFY_THRESHOLD. À ce stade, il n'est pas rentable de conserver le bac sous forme de red-black tree node, mais LinkedListplutôt comme

 entry.next.next....

MIN_TREEIFY_CAPACITY est le nombre minimum de compartiments avant qu'un certain compartiment ne soit transformé en arbre.


10

TreeNodeest une autre façon de stocker les entrées qui appartiennent à une seule case du fichier HashMap. Dans les implémentations plus anciennes, les entrées d'un bac étaient stockées dans une liste chaînée. Dans Java 8, si le nombre d'entrées dans un bac a dépassé un seuil ( TREEIFY_THRESHOLD), elles sont stockées dans une structure arborescente au lieu de la liste chaînée d'origine. Ceci est une optimisation.

De la mise en œuvre:

/*
 * Implementation notes.
 *
 * This map usually acts as a binned (bucketed) hash table, but
 * when bins get too large, they are transformed into bins of
 * TreeNodes, each structured similarly to those in
 * java.util.TreeMap. Most methods try to use normal bins, but
 * relay to TreeNode methods when applicable (simply by checking
 * instanceof a node).  Bins of TreeNodes may be traversed and
 * used like any others, but additionally support faster lookup
 * when overpopulated. However, since the vast majority of bins in
 * normal use are not overpopulated, checking for existence of
 * tree bins may be delayed in the course of table methods.

pas exactement vrai. S'ils réussissent TREEIFY_THRESHOLD ET le nombre total de bacs est au moins MIN_TREEIFY_CAPACITY. J'ai essayé de couvrir cela dans ma réponse ...
Eugene

3

Vous auriez besoin de le visualiser: disons qu'il existe une clé de classe avec uniquement la fonction hashCode () remplacée pour toujours renvoyer la même valeur

public class Key implements Comparable<Key>{

  private String name;

  public Key (String name){
    this.name = name;
  }

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

  public String keyName(){
    return this.name;
  }

  public int compareTo(Key key){
    //returns a +ve or -ve integer 
  }

}

puis ailleurs, j'insère 9 entrées dans un HashMap avec toutes les clés étant des instances de cette classe. par exemple

Map<Key, String> map = new HashMap<>();

    Key key1 = new Key("key1");
    map.put(key1, "one");

    Key key2 = new Key("key2");
    map.put(key2, "two");
    Key key3 = new Key("key3");
    map.put(key3, "three");
    Key key4 = new Key("key4");
    map.put(key4, "four");
    Key key5 = new Key("key5");
    map.put(key5, "five");
    Key key6 = new Key("key6");
    map.put(key6, "six");
    Key key7 = new Key("key7");
    map.put(key7, "seven");
    Key key8 = new Key("key8");
    map.put(key8, "eight");

//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry 

    Key key9 = new Key("key9");
    map.put(key9, "nine");

  threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.

                  key1
                 /    \
               key2   key3
              /   \   /  \

Le parcours de l'arbre est plus rapide {O (log n)} que LinkedList {O (n)} et à mesure que n grandit, la différence devient plus significative.


Il ne peut pas créer d'arborescence efficace car il n'a aucun moyen de comparer des clés autres que leurs codes de hachage, qui sont tous identiques, et leur méthode equals, ce qui n'aide pas à classer.
user253751

@immibis Leurs hashcodes ne sont pas forcément les mêmes. Ils sont probablement différents. Si les classes l'implémentent, il utilisera en plus compareTofrom Comparable. identityHashCodeest un autre mécanisme qu'il utilise.
Michael

@Michael Dans cet exemple, tous les hashcodes sont nécessairement les mêmes et la classe n'implémente pas Comparable. identityHashCode n'aura aucune valeur pour trouver le bon nœud.
user253751

@immibis Ah oui, je l'ai seulement écrémé mais tu as raison. Donc, comme Keyne met pas en œuvre Comparable, identityHashCodesera utilisé :)
Michael

@EmonMishra malheureusement, le simple visuel ne suffira pas, j'ai essayé de le couvrir dans ma réponse.
Eugene

2

Le changement dans l'implémentation de HashMap a été ajouté avec JEP-180 . Le but était de:

Améliorez les performances de java.util.HashMap dans des conditions de collision de hachage élevée en utilisant des arbres équilibrés plutôt que des listes liées pour stocker les entrées de carte. Implémenter la même amélioration dans la classe LinkedHashMap

Cependant, la performance pure n'est pas le seul gain. Cela empêchera également les attaques HashDoS , au cas où une carte de hachage serait utilisée pour stocker les entrées de l'utilisateur, car l' arbre rouge-noir utilisé pour stocker les données dans le compartiment a le pire cas de complexité d'insertion en O (log n). L'arbre est utilisé après qu'un certain critère est satisfait - voir la réponse d'Eugene .


-1

Pour comprendre l'implémentation interne de hashmap, vous devez comprendre le hachage. Le hachage dans sa forme la plus simple est un moyen d'attribuer un code unique à n'importe quelle variable / objet après avoir appliqué n'importe quelle formule / algorithme sur ses propriétés.

Une vraie fonction de hachage doit suivre cette règle -

«La fonction de hachage doit renvoyer le même code de hachage à chaque fois que la fonction est appliquée sur des objets identiques ou égaux. En d'autres termes, deux objets égaux doivent produire le même code de hachage de manière cohérente. »


Cela ne répond pas à la question.
Stephen C
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.