【2023】Analyse et interprétation détaillées du code source HashMap

Préface

Avant de comprendre HashMap, présentons d'abord la structure de données utilisée. Après jdk1.8, la structure de données arborescente rouge-noir a été ajoutée à HashMap afin d'optimiser l'efficacité.

Arbre

En informatique, un arbre est un type de données abstrait (ADT) ou une structure de données qui implémente ce type de données abstrait , utilisé pour simuler une collection de données avec des propriétés de structure arborescente. C'est un ensemble de relations hiérarchiques composées de n (n>0) nœuds limités . On l'appelle « arbre » parce qu'il ressemble à un arbre à l'envers, ce qui signifie qu'il a les racines tournées vers le haut et les feuilles tournées vers le bas. Il présente les caractéristiques suivantes :

  • Chaque nœud a peu ou pas de nœuds enfants ;
  • Un nœud sans nœud parent est appelé nœud racine ;
  • Chaque nœud non racine a exactement un nœud parent ;
  • À l'exception du nœud racine, chaque nœud enfant peut être divisé en plusieurs sous-arbres disjoints ;
  • Il n'y a pas de cycle dans l'arbre
    Insérer la description de l'image ici

La classification comprend les arbres binaires, les arbres de recherche binaires, les arbres rouge-noir, les arbres B, les arbres B+, etc.

1. Arbre binaire

  • Chaque nœud contient au plus deux nœuds enfants, à savoir le nœud enfant gauche et le nœud enfant droit.
  • Il n'est pas nécessaire que chaque nœud ait deux nœuds enfants. Certains n'ont que des enfants gauches et d'autres n'ont que des enfants droits.
  • Le sous-arbre gauche et le sous-arbre droit de chaque nœud de l'arbre binaire satisfont également respectivement aux deux premières définitions.
    Insérer la description de l'image ici

2. Arbre de recherche binaire

  • À n’importe quel nœud de l’arborescence, la valeur de chaque nœud de son sous-arbre gauche doit être inférieure à la valeur de ce nœud, et la valeur du nœud du sous-arbre droit doit être supérieure à la valeur de ce nœud.
  • Il n'y aura pas de nœuds avec des clés égales
  • Normalement, la complexité temporelle de la recherche dans l'arbre binaire est O (log n)
    Insérer la description de l'image ici
    Comme il ne peut pas tourner, le pire des cas se produira, dans lequel les sous-arbres gauche et droit seront extrêmement déséquilibrés.
    Insérer la description de l'image ici

3. Arbre rouge-noir

  • Les nœuds sont rouges ou noirs
  • le nœud racine est noir
  • Les nœuds feuilles sont tous des nœuds vides noirs
  • Les nœuds enfants d’un nœud rouge dans un arbre rouge-noir sont tous noirs.
  • Tous les chemins d'un nœud vers un nœud feuille contiennent le même nombre de nœuds noirs.
  • Après l'ajout ou la suppression, si les cinq définitions ci-dessus ne sont pas remplies, une opération d'ajustement de rotation aura lieu.
  • La complexité temporelle des opérations de recherche, de suppression et d'ajout est O (log n)
    Insérer la description de l'image ici

table de hachage

Une table de hachage, également connue sous le nom de table de hachage , est une structure de données qui accède directement à la valeur (valeur) à un emplacement de stockage en mémoire en fonction d'une clé. Elle a évolué à partir d'un tableau et utilise la fonctionnalité d'un tableau

pour prendre en charge l'accès aléatoire . aux données basées sur des indices. La fonction qui mappe les clés aux indices du tableau est appelée fonction de hachage . Il peut être exprimé comme suit : hashValue = hash(key)
Exigences de base de la fonction de hachage :

  • La valeur de hachage calculée par la fonction de hachage doit être un entier positif supérieur ou égal à 0, car hashValue doit être utilisée comme indice du tableau.
  • Si key1 = key2, alors la valeur de hachage obtenue après le hachage doit également être la même : hash(key1) = hash(key2)
  • Si key1 != key2, alors la valeur de hachage obtenue après le hachage doit également être la même : hash(key1) != hash(key2)

collision de hachage

Dans les situations réelles, il est presque impossible de trouver une fonction de hachage capable de calculer différentes valeurs de hachage pour différentes clés. Cela provoquera un phénomène où plusieurs clés sont mappées à la même position d'indice du tableau après avoir été converties par une opération de hachage. Cette situation est appelée conflit de hachage (ou conflit de hachage ou collision de hachage).
Insérer la description de l'image ici

méthode de fermeture éclair

Afin de résoudre les conflits de hachage, une méthode appelée méthode zipper est généralement utilisée.
Dans une table de hachage, chaque position d'indice du tableau peut être appelée un bucket. Chaque bucket correspond à une liste chaînée. Tous les éléments ayant la même valeur de hachage sont placés dans la liste chaînée correspondant au même slot. Cette méthode Méthode Zipper

  • Lors de l'opération d'insertion, l'emplacement de hachage correspondant est calculé via la fonction de hachage et inséré dans la liste chaînée correspondante. La complexité temporelle de l'insertion est O(1)
  • Lors de la recherche ou de la suppression d'un élément, nous calculons également l'emplacement correspondant via la fonction de hachage, puis parcourons la liste chaînée pour le trouver ou le supprimer.
    • En moyenne, la complexité temporelle de la requête lors de la résolution des conflits basée sur la méthode des listes chaînées est O(1)
    • La table de hachage peut dégénérer en une liste chaînée et la complexité temporelle de la requête dégénérera de O(1) à O(n)
    • Transformez la liste chaînée dans la méthode de liste chaînée en d'autres structures de données dynamiques efficaces, telles que des arbres rouge-noir. La complexité temporelle de la requête est O(log n)
      Insérer la description de l'image ici
      Insérer la description de l'image ici

Et l’utilisation d’arbres rouge-noir peut prévenir efficacement les attaques DDos.

1. Introduction

HashMapIl s'agit d'une classe d'implémentation importante dans Map. Il s'agit d'une table de hachage et le contenu stocké est un mappage de paires clé-valeur (clé => valeur). HashMap n'est pas thread-safe. HashMap permet le stockage de clés et de valeurs nulles, et les clés sont uniques.

Avant JDK1.8, HashMapla structure de données sous-jacente était une pure structure de tableau + liste chaînée. Étant donné que les tableaux ont les caractéristiques d'une lecture rapide et d'un ajout et d'une suppression lents, tandis que les listes chaînées ont les caractéristiques d'une lecture lente et d'un ajout et d'une suppression rapides, HashMap combine les deux sans utiliser de verrous de synchronisation pour la modification, ses performances sont donc meilleures. Le tableau est HashMaple corps principal et une liste chaînée est introduite pour résoudre les conflits de hachage. La méthode spécifique pour résoudre les conflits de hachage ici est : méthode zipper

2. Analyse du code source

1. mettre la méthode

1.1. Attributs communs

Seuil d'expansion = capacité du réseau * facteur de charge

    //默认的初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  
    //默认的加载因子     
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //存储数据的数组
    transient Node<K,V>[] table;
    //容量
    transient int size;    
    

1.2. Constructeur

    //默认无参构造
    public HashMap() {
    
    
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 指定加载因子为默认加载因子 0.75
    }
  • HashMap crée des tableaux paresseusement et n'initialise pas le tableau lors de la création de l'objet.
  • Dans le constructeur sans paramètre, le facteur de chargement par défaut est défini

1.3.méthode put

  • organigramme
    Insérer la description de l'image ici
  • Code source spécifique
    
	public V put(K key, V value) {
    
    
        return putVal(hash(key), key, value, false, true);
    }
    
	/** 
	*  计算hash值的方法
	*/
		static final int hash(Object key) {
    
    
	        int h;
	        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	    }

	/** 
	*  具体执行put添加方法
	*/
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断数组是否初始化(数组初始化是在第一次put的时候)
        if ((tab = table) == null || (n = tab.length) == 0)
        	//如果未初始化,调用resize()进行初始化
            n = (tab = resize()).length;
        //通过 & 运算符计算求出该数据(key)的数组下标并且判断该下标位置是否有数据
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//如果没有,直接将数据放在该下标位置
            tab[i] = newNode(hash, key, value, null);
        else {
    
      //该下标位置有数据的情况
            Node<K,V> e; K k;
            //判断该下标位置的数据是否和当前新put的数据一样
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
				//如果一样,则直接覆盖value
                e = p;
            //判断是不是红黑树
            else if (p instanceof TreeNode)  
            	//如果是红黑树的话,进行红黑树的具体添加操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果都不是代表是链表
            else {
    
      
            	//遍历链表
                for (int binCount = 0; ; ++binCount) {
    
    
                	//判断next节点是否为null,是null代表遍历到链表尾部了
                    if ((e = p.next) == null) {
    
    
                    	//把新值插入到尾部
                        p.next = newNode(hash, key, value, null);
                        //插入数据后,判断链表长度有大于等于8了没
                        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;
                }
            }
            //判断e是否为null(e值为前面修改操作存放原数据的变量)
            if (e != null) {
    
     // existing mapping for key
            	//不为null的话证明是修改操作,取出老值
                V oldValue = e.value;
               
                if (!onlyIfAbsent || oldValue == null)
                	//把新值赋值给当前节点
                    e.value = value;
                afterNodeAccess(e);
                //返回老值
                return oldValue;
            }
        }
        //计算当前节点的修改次数
        ++modCount;
        //判断当前数组中的数据量是否大于扩容阈值
        if (++size > threshold)
        	//进行扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • processus spécifique
  1. Déterminez si la table du tableau clé-valeur est nulle et effectuez une exécution complexe de resize() pour l'expansion (initialisation)
  2. Calculez la valeur de hachage en fonction de la paire clé-valeur pour obtenir l'index du tableau
  3. Juger la table[i]==valeur de hachage pour obtenir l'index du tableau
  4. Si taale[i] == null, la condition est établie, créez directement un nouveau nœud et ajoutez
    i. Déterminez si le premier élément de table[i] est identique à la clé. Si c'est le cas, écrasez directement la valeur
    ii. Déterminez si table[i] est un treeNode, c'est-à-dire que table [i] est-il un arbre rouge-noir ? S'il s'agit d'un arbre rouge-noir, insérez directement la paire clé-valeur dans le nombre iii. Parcourez table[i],
    insérez données à la fin de la liste chaînée, puis déterminez si la longueur de la liste chaînée est supérieure à 8. Si tel est le cas, convertissez la liste chaînée Il s'agit d'une opération d'arborescence rouge-noir. S'il s'avère que la clé existe déjà pendant le processus de traversée, la valeur sera écrasée.

1.4 méthode de redimensionnement (extension de capacité)

  • organigramme
    Insérer la description de l'image ici
  • Code source spécifique
    final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;
        //如果当前数组为null的时候,把oldCap 老数组容量设置为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //老的扩容阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //判断数组容量是否大于0,大于0说明数组已经初始化
        if (oldCap > 0) {
    
    
        	//判断当前数组长度是否大于最大数组长度
            if (oldCap >= MAXIMUM_CAPACITY) {
    
    
            	//如果是,将扩容阈值直接设置为int类型的最大数值并且直接返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果在最大长度访问内,则需要扩容oldCap << 1 == oldCap * 2
            //并且判断是否大于16,
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold  等价于 oldCap * 2
        }
      
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //数组初始化的情况,将阈值和扩容因子设置为默认值
        else {
    
                   // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //初始化容量小于16的时候,扩容阈值没用阈值的
        if (newThr == 0) {
    
    
        	//创建阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //计算出来的阈值赋值
        threshold = newThr;
        @SuppressWarnings({
    
    "rawtypes","unchecked"})
        //根据上边计算得出的容量 创建新的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //扩容操作,判断不为null证明不是初始化数组
        if (oldTab != null) {
    
    
        //	遍历数组
            for (int j = 0; j < oldCap; ++j) {
    
    
                Node<K,V> e;
                //判断当前下标为j的数组如果不为null的话赋值给e
                if ((e = oldTab[j]) != null) {
    
    
                	//将数组的位置设置为null
                    oldTab[j] = null;
                    //判断是否有下一个节点
                    if (e.next == null)
                    	//如果没有,就查询计算在新数组中的下标并放进去
                        newTab[e.hash & (newCap - 1)] = e;
                    //有下个节点的情况,并且判断是否已经树化
                    else if (e instanceof TreeNode)
                    	//进行红黑树的操作
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                   //有下个节点的情况,并且判还没有树化  
                    else {
    
     // preserve order
                        Node<K,V> loHead = null, loTail = null;  //低位数组
                        Node<K,V> hiHead = null, hiTail = null;  //高位数组
                        Node<K,V> next;
                        遍历循环
                        do {
    
    
                        	//取出next节点
                            next = e.next;
                            //通过 & 操作计算出结果为0
                            if ((e.hash & oldCap) == 0) {
    
    
                            	//如果低位为null,则把e值放入低位2头
                                if (loTail == null)
                                    loHead = e;
                                //低位尾不是null,
                                else
                                	//将数据放入next节点
                                    loTail.next = e;
                                loTail = e;
                            }
                            
                            else {
    
    
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位如果记录的有数据,是链表
                        if (loTail != null) {
    
    
                        //将下一个元素置空
                            loTail.next = null;
                            //将低位头放入新数组的
                            newTab[j] = loHead;
                        }
                        //高位尾如果记录有数据,是链表
                        if (hiTail != null) {
    
    
                        	//将下个元素置空
                            hiTail.next = null;
                            //将高位头放入新数组的(原下标+原数组容量)位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • Principe d'exécution
  1. Lors de l'ajout d'éléments ou de l'initialisation, vous devez appeler la méthode resize pour l'expansion. La première fois que vous ajoutez des données, la longueur initiale du tableau est de 16. Chaque expansion ultérieure atteindra le seuil d'expansion (longueur du tableau * 0,75).
  2. Chaque fois que la capacité est augmentée, la capacité sera le double de la capacité avant l'expansion ;
  3. Après l'expansion, un nouveau tableau sera créé et les données de l'ancien tableau doivent être déplacées vers le nouveau tableau
    i. Pour les nœuds sans conflits de hachage, utilisez directement e.hash&(newCap-1) pour calculer la position d'index du nouveau tableau
    ii. S'il s'agit d'un arbre rouge-noir et l'ajout de l'arbre rouge-noir
    iii. S'il s'agit d'une liste chaînée, vous devez parcourir la liste chaînée et vous devrez peut-être diviser la liste chaînée pour déterminer si (e.hash&oldCap) vaut 0. La position de l'élément restera soit à la position d'origine, soit se déplacera vers la position d'origine + la taille du tableau augmentée.

Comment redéterminer la position des éléments dans le tableau lors de l'expansion ?On voit qu'elle est déterminée par if ((e.hash & oldCap) == 0).

hash HEX(97)  = 0110 0001‬ 
n    HEX(16)  = 0001 0000
--------------------------
         结果  = 0000 0000
# e.hash & oldCap = 0 计算得到位置还是扩容前位置
     hash HEX(17)  = 0001 0001‬ 
     n    HEX(16)  = 0001 0000
--------------------------
         结果  = 0001 0000
#  e.hash & oldCap != 0 计算得到位置是扩容前位置+扩容前容量

obtenir la méthode

public V get(Object key) {
    
    
    // 定义一个Node结点
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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;
        // bucket中不止一个结点
        if ((e = first.next) != null) {
    
    
            //判断是否为TreeNode树结点
            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);
        }
    }
    // 找不到就返回null
    return null;
}

Problème commun

1. Comment est calculé l’indice ? Maintenant que nous avons hashCode, pourquoi devons-nous encore utiliser la méthode hash() ? Pourquoi la capacité du tableau 2 est-elle à la nième puissance ?

  • Calculez d'abord le hashCode() de la clé, puis appelez Hash()la méthode et utilisez la méthode XOR pour perturber l'opération de hachage secondaire, et enfin (n - 1) & hashobtenez l'index via l'opération AND. (L'utilisation de cette méthode équivaut à une opération de hachage % n modulo)

  • Le but de hash() secondaire est de synthétiser des données binaires de grande taille, de rendre la distribution de hachage plus uniforme et de réduire la probabilité de collision de hachage. La formule de calcul est la suivante :(h = key.hashCode()) ^ (h >>> 16)

  • Pour calculer l'indice, s'il s'agit de la nième puissance de 2, vous pouvez utiliser l'opération AND au niveau du bit au lieu de modulo, ce qui est plus efficace ; et lorsque la capacité est étendue, les éléments avec hash & lodCap == 0 resteront à l'original. position, et ceux avec 1 seront renvoyés à la position développée. nouvelle position, nouvelle position = ancienne position + lodCap

    • 计算方式 :hash & length Utilisez la valeur de hachage secondaire et la capacité d'origine pour effectuer des opérations. Si le résultat est 0, la position restera inchangée. S'il n'est pas 0, il se déplacera vers une nouvelle position.
    • La nouvelle méthode de calcul de position :原始数组容量+原始下标=新的位置
  • L'objectif principal de l'utilisation de la nième puissance de 2 est de mieux coordonner l'efficacité de l'optimisation et de répartir les indices plus uniformément.

2. Quelle est la différence entre 1,7 et 1,8 dans le processus de la méthode put de HashMap ?

  1. HashMap crée des tableaux paresseusement. Le tableau n'est créé qu'après la première utilisation.
  2. Calculer l'index (indice du compartiment)
    1. Commencez par obtenir la valeur de hachage de la clé, puis calculez la valeur de hachage secondaire via la méthode hash().**La méthode de calcul est la suivante : ** (h = key.hashCode()) ^ (h >>> 16)Déplacez la valeur de hachage vers la droite via non signé, puis effectuez le calcul XOR avec le valeur de hachage d'origine. , cette fonction consiste principalement à perturber les 16 bits inférieurs qui participent réellement au calcul, ce qui peut efficacement perturber le fonctionnement et réduire la probabilité de collision de hachage.
    2. Utilisez ensuite toute la valeur de hachage secondaire et la capacité du tableau à diviser et quittez la méthode du reste. Le reste obtenu est l'indice final du bucket. La méthode de calcul est la suivante : prendre la longueur du(n-1)&hash tableau -1 et effectuer une opération ET avec le valeur de hachage. Cela équivaut à utiliser la valeur de hachage pour créer le reste % % avec la longueur n du tableau.L'exploitation, l'utilisation et l'exécution d'opérations visent principalement à améliorer efficacement l'efficacité des opérations (1.7 n'a pas cette optimisation)
  3. Si l'indice du bucket n'est occupé par personne, créez un nœud Node et renvoyez
  4. Si l'indice du bucket est déjà occupé : il sera comparé à chaque nœud un par un pour voir si la valeur de hachage et equals() sont relatifs. S'ils sont égaux, cela signifie la même clé, alors écrasez-la et modifiez-la. S'ils sont égaux, cela signifie la même clé, alors écrasez-la et modifiez-la. ne sont pas les mêmes, ajoutez-le.
    1. Ajouter ou mettre à jour la logique lorsque TreeNode est déjà un arbre rouge-noir
    2. S'il s'agit d'un nœud ordinaire, il utilisera la logique d'ajout ou de mise à jour de la liste chaînée. Si la longueur de la liste chaînée dépasse le seuil d'arborescence de 8, il utilisera la logique d'arborescence et effectuera l'opération d'arborescence (la condition préalable est que la longueur du tableau atteint 64)
  5. Avant de revenir, il vérifiera également si la capacité dépasse le seuil d'expansion (longueur de la baie/facteur de chargement).Une fois dépassé, la capacité sera étendue.
  6. différent:
    1. Lors de l'insertion dans une liste chaînée, 1.7 utilise la méthode d'insertion de tête (insertion à partir de la tête de la liste chaînée) et 1.8 utilise la méthode d'insertion de queue (insertion à partir de la fin de la liste chaînée).
      1. 1,7 signifie une expansion lorsqu'elle est supérieure ou égale au seuil et qu'il n'y a pas d'espace (s'il y a de l'espace, il ne sera pas agrandi mais continuera à être placé à l'indice de bucket calculé), tandis que 1,8 signifie une expansion lorsqu'il est supérieur supérieur ou égal au seuil.
      2. 1.8 Lors de l'extension du nœud de calcul, il optimisera

Je suppose que tu aimes

Origine blog.csdn.net/weixin_52315708/article/details/131918897
conseillé
Classement