HashMap底层原理

5.4 HashMap底层原理

5.4.1初始长度

// 0000 0001 << 4 = 0001 0000 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

HashMap的初始长度为16,当不够用时,再扩展,但始终是2的幂次。

5.4.2 put操作原理

/**
** put(K,V) -----> putVal(hash,k,v)
**/
  1. 计算key对应的hash值;

    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        } 
    
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    备注:

    hash()为什么要有^(h >>> 16)?
    答:为了将hashCode的高位和低位一起异或运算,这样算出来的结果分散率较高,避免hash碰撞问题。
    h:是int类型的,四个字节,32bit,是通过key(Object类型,Object有一个hashcode()方法获取key的hashCode值)的值计算出来的hash值。
    假如            h =    00011010 11000110 11010001 01010111 
              h>>>16 =    00000000 00000000 00011010 11000110  (按位右移补零操作符)
        h ^ (h>>>16) =    00011010 11000110 11001011 10010001 (如果相对应位值相同,则结果为0,否则为1)
    
    &	如果相对应位都是1,则结果为1,否则为0	(A&B),得到12,即0000 1100
    |	如果相对应位都是 0,则结果为 0,否则为 1	(A | B)得到61,即 0011 1101
    ^	如果相对应位值相同,则结果为0,否则为1	(A ^ B)得到49,即 0011 0001
    〜	按位取反运算符翻转操作数的每一位,即0变成1,1变成0。	(〜A)得到-61,即1100 0011
    << 	按位左移运算符。左操作数按位左移右操作数指定的位数。	A << 2得到240,即 1111 0000
    >> 	按位右移运算符。左操作数按位右移右操作数指定的位数。	A >> 2得到15即 1111
    >>> 	按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。	A>>>2得到15即0000 1111
    
  2. 初始化一个长度为16的Node<K,V> []数组;

    if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
    
  3. 根据hash值计算数组下标,判断当前数组是否存有值

    (1)当前位置没有值:则new一个node<k,v>存入。

    if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
    

    备注:

    //*********************注*************************//
    // 数组下标i可以直接取余n(数组长度),如n==16,i = hash%16 = 0~15
    但是为什么要有(n-1) & hash ?
      如   n-1 = 15 : 0000 1111     
                hash: 0101 0101                           (随着key的不同而不同)
                    &
                      0000 0101
     (1)无论hash值如何变,最终算的i都是低位,即0~(n-1)之间。
     (2)由于hash值随着key的不同而不同,所以这样算出来的i分布比较规律。(若使用hash%n算,可能算出的结果是某个i存了很多数据,其他i又没有存任何数据)
    
    (3)规定数组的大小为2的幂次,如32,64....,为了方便计算i的值,使其规律分布。
                 如   n-1 = 31: 0001 1111
                        hash: 0110 1010
                            &
                              0000 1010
                            
    (4)当初始化一个HashMap的长度n时,会返回一个大于等于n的2的幂次。
     // 如 Map<String,String> map = new HashMap<>(10);
     // 实际上内部数组的大小为16 = 2^4
     
    

    (2)当前位置有值:判断key是否相同

    else {
                Node<K,V> e; K k;
        
        //判断当前位置与要put进去得hash值、key值是否相同
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;//相同直接等于e
            }
    

    当前位置hash值和key都相同,但是value不同:覆盖新的值,返回旧的值

                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                     //如果onlyIfAbsent为true,或者旧的值为null,则覆盖新的值
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
    

  
    //key不同 
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //(1)
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                // (3)  
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash); //转为红黑树
                        break;
                    }
                 // (2)if() 判断key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

备注:

(1)for循环?
	遍历table数组某个位置上的链表,进行尾插法,遍历个数,当链表个数大于8,则需要将链表改为红黑树。
(2)又有 if() 判断key值是否相等?
    第一次是判断链表第一个节点的key值是否与put进来的相等,而for循环里面的if是判断链表里面的key值是否与put进来的相等。
(3)TREEIFY_THRESHOLD = 8,当binCount>7时,表示当前链表上已经有8个节点了,此时需要将链表转为红黑树,而新put进来的node节点是先加到链表上,再去转为红黑树(此时已有9个节点)。(jdk1.8)

  1. 扩容

    //每进行修改(put、modify)一次,都会去判断当前容量是否充足,threshhold为阈值。
    ++modCount;
    if (++size > threshold)
       resize();
    afterNodeInsertion(evict);
    

    resize()扩容

    1.jdk1.7
    直接新建一个数组,将老数组中的每一个节点直接转移到新数组。
    
    2.jdk1.8
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                //太大了,大到超过最大值时,不允许扩容。
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //原数组的容量翻一倍(<<1)小于最大容量,且原数组容量已超过默认值
                //新数组容量翻倍,新阈值也翻倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
        	//自己指定HashMap大小的时候
            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);
            }
            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;
            if (oldTab != null) {
                //遍历整个数组(tab)
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    //发现数组某个位置存储有值
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        //这个位置只存了一个节点(既不是链表也不是红黑树)
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        //这个位置上存储的是红黑树  看注3
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //这个位置上存储的是链表
                        else { 
                            //高位与低位看注1
                            Node<K,V> loHead = null, loTail = null;//低位头尾节点
                            Node<K,V> hiHead = null, hiTail = null;//高位头尾节点
                            Node<K,V> next;
                            //遍历这个位置上的链表
                            do {
                                next = e.next;
                                //此处看注2
                                //如果这个节点是低位的,则给低位链表(尾插法)
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        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;
        }
    

    注1:

    高位和低位的出现:
    	由于存储的数组索引是根据(n-1)& hash计算出来的,n是数组的容量大小,由于扩容之后数组的大小改变,所以某数据存储在原数组的索引与扩容之后存储的不同。
    	前面分析知道扩容后大小变大一倍。假如原数组大小为n=16,则扩容后变为32.
    例1:hash=0110 1010
       15: 0000 1111            31: 0001 1111            
     hash: 0110 1010          hash: 0110 1010
    	&
      索引: 0000 1010                0000 1010
       可以看出扩容之后所得到索引与原数组的相同。
          
    例2:hash=0111 1010  (改变了第四个数为1,称“特殊位”)
      15:0000 1111             31: 0001 1111            
    hash: 0111 1010           hash: 0111 1010          
    	&
     索引: 0000 1010                0001 1010                
        可以看出扩容之后所得到的索引与原数组存储的索引相差16,即相差原数组大小的距离。
          
    结论:扩容之后,原来某个数据所存储的位置索引i可能与原来的位置相同(称为低位),也可能与原位置相差n大小(称为高位)。
          即:相等 / 原+原数组大小
          即:看扩容后节点存储在高位还是低位,关键在hash值的特殊位是0还是1,若是0,则低位,否则,为高位。
    

    注2:

    看扩容后节点存储在高位还是低位,关键在hash值的”特殊位“是0还是1,若是0,则低位,否则,为高位。
    
    (1)判断此节点存储在低位
    (e.hash & oldCap) == 0  //oldCap:是原数组的大小
        
     例1:  假设oldCap = 16
        16:  0001 0000
      hash:  0110 1010
          &
        值:  0000 0000    =  0  存低位
            
    (2)判断此节点存储高位
            
      例2:  假设oldCap = 16
        16:  0001 0000
      hash:  0111 1010
          &
        值:  0001 0000    !=  0  存高位
         
    

    总结:链表

    如果当前位置存储的是链表,在扩容时,我们首先得遍历链表得每一个节点,然后判断当前节点是存储在低位还是高位,低位的生成一个新的链表,然后存储到新数组的低位,高位的生成一个新的链表,然后存储到高位。
    低位是指当前节点的hash值与原数组大小进行&运算,若为0则为低位,否则为高位。
    

    注3:红黑树

    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    

    TreeNode:

变化后如上图,一个几点有:next、pre、left、right
//思路和此位置存的是链表的代码相同(看链表)

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            //遍历红黑树(循环链表)
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) { //低位
                    if ((e.prev = loTail) == null) //第一个
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else { //高位
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                ////UNTREEIFY_THRESHOLD=6 
                if (lc <= UNTREEIFY_THRESHOLD) 
                    //低位个数小于6时,转化为链表(退化)
                    tab[index] = loHead.untreeify(map);
                else {
                    //不满足条件时,先把头节点存在新数组位置,在判断是否要重新转化为一个新的红黑树
                    //当hiHead!=null时,表示有节点存在高位,而存在低位的节点需要重新生成一个红黑树
                    //当hiHead==null时,表示原数组这个位置的所有节点都存在新数组的低位,这样就不用重新生成一个新的红黑树,直接让头节点等于就行了,它自己本来就是一个红黑树了。
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

总结:红黑树

如果当前位置存储的是红黑树,那么扩容时,我需要去遍历当前位置的红黑树的每个TreeNode,并统计存储在低位和高位的个数。如果存在低位的个数 <= 6,就需要将存在低位的TreeNode转化为Node,存在新数组低位的链表中。
如果存在低位的个数 > 6,我们先要判断高位是否有数据,如果没有的话,直接将红黑树存在新数组低位就可以了。
如果有的话,我们就得将存在低位的节点重新生成一个新的红黑树。

5.红黑树(TreeNode)
(1)红黑树的特点:

1.每个节点是红色或黑色的。
2.根节点必须是黑色的。
3.叶子节点是黑色的并且是空节点。
4.节点是红色的,那么他的两个儿子都是黑色。(不能连续出现两个红色节点)
5.对于每个节点,从该节点到其子孙节点的路径上包含的黑节点数目相同。

(2)新增一个节点:

1.新节点是黑色的,不需要调整。
2.新节点是红色的:需要变换(变色+旋转)

(3)变换规则:

1.变色:新增节点的父亲节点和叔叔节点是红色。
	(1)把父亲节点、叔叔节点变为黑色;
	(2)把祖父(父亲的父亲)节点变为红色;
2.旋转:当前节点的父亲节点是红色,而叔叔节点是黑色。
	(1)左旋:当前节点是右子树
			以父节点进行左旋
	(2)右旋:当前节点是左子树
			以祖父节点进行右旋
			将父节点变为黑色,祖父节点变为红色。

例子:

①第一步,是否符合红黑树特点:新增节点2,为红色的,树5和2节点不符合红黑树特点4,需要变换。

②第二步,是否需要变色:父节点5和叔叔节点9都是红色,需要变色。

​ 把父节点和叔叔节点变为黑色,祖父节点变为黑色。

③第三步,是否符合红黑树特点:8不符合。变色的条件也不满足,此时就要旋转了。

④第四步,左旋:以父节点左旋。(父节点以及其左子树整个往下,右节点往上充当父节点,把右节点多余的左子树给原父节点,过程自己琢磨)

⑤第五步,是否符合红黑树特点:5和8不符合,且红节点5位于左子树,要进行右旋。

⑥第六步,右旋:以祖父节点右旋。(祖父节点以及其右子树整个往下,祖父节点的左节点此时往上充当新的祖父节点,并将多余的分支给原祖父节点)

posted @ 2022-10-08 20:06  湘summer  阅读(54)  评论(0编辑  收藏  举报