Selon le document de lien suivant: Java HashMap Implementation
Je suis confus avec l'implémentation de HashMap
(ou plutôt d'une amélioration dans HashMap
). Mes questions sont:
Tout d'abord
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
Pourquoi et comment utilise-t-on ces constantes? Je veux quelques exemples clairs pour cela. Comment ils réalisent un gain de performance avec cela?
Deuxièmement
Si vous voyez le code source de HashMap
dans 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 .
HashMap
contient un certain nombre de compartiments. Il utilise hashCode
pour déterminer le compartiment dans lequel les placer. Pour simplifier, imaginez-le comme un module.
Si notre hashcode est 123456 et que nous avons 4 seaux, 123456 % 4 = 0
donc l’article entre dans le premier compartiment, Bucket 1.
Si notre fonction de hashcode est bonne, elle devrait fournir une distribution égale afin que tous les compartiments soient utilisés de la même manière. Dans ce cas, le compartiment utilise une liste liée pour stocker les valeurs.
Mais vous ne pouvez pas compter sur les gens pour mettre en œuvre de bonnes fonctions de hachage. Les gens écriront souvent de mauvaises fonctions de hachage, ce qui entraînera une distribution inégale. Il est également possible que nous ne puissions pas avoir de chance avec nos entrées.
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 de résoudre ce problème en organisant certains compartiments dans des arborescences plutôt que dans des listes chaînées si les compartiments deviennent trop volumineux. C'est quoi TREEIFY_THRESHOLD = 8
est pour. Si un seau contient plus de huit éléments, il devrait devenir un 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 méthode compareTo
de Comparable
si 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 ce que le UNTREEIFY_THRESHOLD = 6
est pour. Si le nombre d'éléments dans un compartiment passe au-dessous de six, nous pouvons également utiliser une liste chaînée.
Enfin, il y a le MIN_TREEIFY_CAPACITY = 64
.
Quand une carte de hachage grossit, elle se redimensionne automatiquement pour avoir plus de compartiments. Si nous avons une petite carte de hachage, la probabilité d'obtenir des compartiments très complets est assez élevée, car nous n'avons pas beaucoup de compartiments différents dans lesquels placer des éléments. Il est bien mieux d'avoir une carte de hachage plus grande, avec plus de seaux moins pleins. Cette constante dit fondamentalement de ne pas commencer à transformer des seaux en arbres si notre carte de hachage est très petite - elle devrait plutôt ê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 cas le plus défavorable . Je ne fais que spéculer, mais vous ne verriez probablement qu'une amélioration notable des performances à cause de ces optimisations si votre fonction hashCode
n'était pas très bonne.
Pour simplifier (autant que je pouvais plus simple) + quelques détails supplémentaires.
Ces propriétés dépendent de nombreuses choses internes qu'il serait très cool de comprendre - avant de passer directement à elles.
TREEIFY_THRESHOLD -> quand un = [célibataire atteint ce nombre (et le nombre total dépasse MIN_TREEIFY_CAPACITY
), il est transformé 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 compartiment/bin avec Integer.MAX_VALUE entrées.
Quelques intro pour le sujet suivant. Pourquoi le nombre de bacs/seaux est-il toujours une puissance de deux? Au moins deux raisons: le fonctionnement plus rapide que modulo et le modulo sur les nombres négatifs seront négatifs. Et vous ne pouvez pas mettre une entrée dans un seau "négatif":
int arrayIndex = hashCode % buckets; // will be negative
buckets[arrayIndex] = Entry; // obviously will fail
Au lieu de cela une astuce de Nice est 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 en tant qu'opération modulo. Il gardera les bits inférieurs. Cela a une conséquence intéressante quand 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 sur les 4 derniers bits uniquement de votre hashcode.
C'est ici qu'intervient la multiplication des seaux. Sous certaines conditions (cela prendrait beaucoup de temps à expliquer dans détails exacts), la taille des seaux est doublée. Pourquoi? Lorsque la taille des seaux est doublée, il reste un bit supplémentaire en jeu.
Donc, vous avez 16 compartiments - les 4 derniers bits du hashcode décident où une entrée va. Vous doublez les compartiments: 32 compartiments - les 5 derniers bits déterminent l’orientation de l’entrée.
En tant que tel, ce processus s'appelle le re-hachage. Cela pourrait être lent. C’est (pour les personnes qui s’inquiètent) que HashMap est "plaisanté" comme: rapide, rapide, rapide, slooow. Il existe d'autres implémentations - search hashmap sans pause ...
Maintenant NTREEIFY_THRESHOLD entre en jeu après un nouveau hachage. À ce stade, certaines entrées peuvent passer de ces bacs à d’autres (elles ajoutent un bit de plus à la (n-1)&hash
calcul - et en tant que tel pourrait se déplacer vers autre seaux) et il pourrait atteindre ce UNTREEIFY_THRESHOLD
. À ce stade, il n’est pas rentable de garder le bac comme red-black tree node
, mais en tant que LinkedList
à la place, comme
entry.next.next....
MIN_TREEIFY_CAPACITY est le nombre minimum de compartiments avant qu'un certain compartiment ne soit transformé en un arbre.
TreeNode
est une autre façon de stocker les entrées appartenant à un seul bac de la HashMap
. Dans les implémentations plus anciennes, les entrées d'une corbeille étaient stockées dans une liste chaînée. Dans Java 8, si le nombre d'entrées dans une corbeille a dépassé un seuil (TREEIFY_THRESHOLD
), ils sont stockés dans une structure arborescente au lieu de la liste lié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.
Vous devez 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
}
}
et 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
/ \ / \
La traversée des arbres est plus rapide {O (log n)} que LinkedList {O (n)} et à mesure que n grandit, la différence devient plus significative.
Le changement d’implémentation HashMap a été ajouté avec JEP-18 . Le but était de:
Améliorez les performances de Java.util.HashMap dans des conditions de collision par hachage élevées en utilisant des arbres équilibrés plutôt que des listes chaînées pour stocker des 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 attaque HashDoS , dans le cas où une carte de hachage est utilisée pour stocker les entrées de l'utilisateur, car arbre rouge-noir qui est utilisé pour stocker des données dans le compartiment a la complexité d'insertion dans le cas le plus défavorable en O (log n). L'arbre est utilisé après qu'un certain critère est rempli - voir réponse d'Eugene .
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 à toute variable/objet après avoir appliqué une formule/un 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 à des objets identiques ou identiques. En d'autres termes, deux objets égaux doivent produire le même code de hachage de manière cohérente. "