zoukankan      html  css  js  c++  java
  • Java HashMap的扩容

    最近博主参加面试,发现自己对于Java的HashMap的扩容过程理解不足,故最近在此进行总结。

    首先说明博主德Java为1.8版本

    HashMap中的变量

    首先要了解HashMap的扩容过程,我们就得了解一些HashMap中的变量:

    • Node<K,V>:链表节点,包含了key、value、hash、next指针四个元素
    • table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体
    • size:记录了放入HashMap的元素个数
    • loadFactor:负载因子
    • threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor
     

    HashMap的构造函数

    HashMap的构造函数主要有四个,代码如下:
    [java] view plain copy
     
    1. public HashMap(int initialCapacity, float loadFactor) {  
    2.     ...  
    3.     this.loadFactor = loadFactor;  
    4.     this.threshold = tableSizeFor(initialCapacity);  
    5. }  
    6.   
    7. public HashMap(int initialCapacity) {  
    8.     this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    9. }  
    10.   
    11. public HashMap() {  
    12.     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted  
    13. }  
    14.   
    15. public HashMap(Map<? extends K, ? extends V> m) {  
    16.     this.loadFactor = DEFAULT_LOAD_FACTOR;  
    17.     putMapEntries(m, false);  
    18. }  

    其中主要有两种形式:
    • 直接拷贝别的HashMap的形式,在此不作讨论
    • 定义初始容量大小(table数组的大小,缺省值为16),定义负载因子(缺省值为0.75)的形式
    值得注意的是,当我们自定义HashMap初始容量大小时,构造函数并非直接把我们定义的数值当做HashMap容量大小,而是把该数值当做参数调用方法tableSizeFor,然后把返回值作为HashMap的初始容量大小:
    [java] view plain copy
     
    1. /** 
    2.  * Returns a power of two size for the given target capacity. 
    3.  */  
    4. static final int tableSizeFor(int cap) {  
    5.     int n = cap - 1;  
    6.     n |= n >>> 1;  
    7.     n |= n >>> 2;  
    8.     n |= n >>> 4;  
    9.     n |= n >>> 8;  
    10.     n |= n >>> 16;  
    11.     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  
    12. }  

    该方法会返回一个大于等于当前参数的2的倍数,因此HashMap中的table数组的容量大小总是2的倍数。

    何时进行扩容?

    HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table:
    [java] view plain copy
     
    1. public V put(K key, V value) {  
    2.     return putVal(hash(key), key, value, false, true);  
    3. }  
    4.   
    5. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  
    6.                boolean evict) {  
    7.     Node<K,V>[] tab; Node<K,V> p; int n, i;  
    8.     if ((tab = table) == null || (n = tab.length) == 0)  
    9.         n = (tab = resize()).length;  
    10.     if ((p = tab[i = (n - 1) & hash]) == null)  
    11.         tab[i] = newNode(hash, key, value, null);  
    12.     else {  
    13.         ...  
    14.     }  
    15.     ++modCount;  
    16.     if (++size > threshold)  
    17.         resize();  
    18.     afterNodeInsertion(evict);  
    19.     return null;  
    20. }  

    在putVal方法第8、9行我们可以看到,当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化
    在putVal方法第16、17行我们可以看到,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容
     
    在这里值得注意的是,在putVal方法第10行我们可以看到,插入元素的hash值是一个32位的int值,而实际当前元素插入table的索引的值为 :
    [java] view plain copy
     
    1. (table.size - 1)& hash  

    又由于table的大小一直是2的倍数,2的N次方,因此当前元素插入table的索引的值为其hash值的后N位组成的值
     

    resize扩容

    [java] view plain copy
     
    1. final Node<K,V>[] resize() {  
    2.     Node<K,V>[] oldTab = table;  
    3.     int oldCap = (oldTab == null) ? 0 : oldTab.length;  
    4.     int oldThr = threshold;  
    5.     int newCap, newThr = 0;  
    6.     if (oldCap > 0) {  
    7.         if (oldCap >= MAXIMUM_CAPACITY) {  
    8.             threshold = Integer.MAX_VALUE;  
    9.             return oldTab;  
    10.         }  
    11.         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
    12.                  oldCap >= DEFAULT_INITIAL_CAPACITY)  
    13.             newThr = oldThr << 1; // double threshold  
    14.     }  
    15.     else if (oldThr > 0) // initial capacity was placed in threshold  
    16.         newCap = oldThr;  
    17.     else {               // zero initial threshold signifies using defaults  
    18.         newCap = DEFAULT_INITIAL_CAPACITY;  
    19.         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
    20.     }  
    21.     if (newThr == 0) {  
    22.         float ft = (float)newCap * loadFactor;  
    23.         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  
    24.                   (int)ft : Integer.MAX_VALUE);  
    25.     }  
    26.     threshold = newThr;  
    27.     @SuppressWarnings({"rawtypes","unchecked"})  
    28.         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  
    29.     table = newTab;  
    30.     if (oldTab != null) {  
    31.         for (int j = 0; j < oldCap; ++j) {  
    32.             Node<K,V> e;  
    33.             if ((e = oldTab[j]) != null) {  
    34.                 oldTab[j] = null;  
    35.                 if (e.next == null)  
    36.                     newTab[e.hash & (newCap - 1)] = e;  
    37.                 else if (e instanceof TreeNode)  
    38.                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
    39.                 else { // preserve order  
    40.                     Node<K,V> loHead = null, loTail = null;  
    41.                     Node<K,V> hiHead = null, hiTail = null;  
    42.                     Node<K,V> next;  
    43.                     do {  
    44.                         next = e.next;  
    45.                         if ((e.hash & oldCap) == 0) {  
    46.                             if (loTail == null)  
    47.                                 loHead = e;  
    48.                             else  
    49.                                 loTail.next = e;  
    50.                             loTail = e;  
    51.                         }  
    52.                         else {  
    53.                             if (hiTail == null)  
    54.                                 hiHead = e;  
    55.                             else  
    56.                                 hiTail.next = e;  
    57.                             hiTail = e;  
    58.                         }  
    59.                     } while ((e = next) != null);  
    60.                     if (loTail != null) {  
    61.                         loTail.next = null;  
    62.                         newTab[j] = loHead;  
    63.                     }  
    64.                     if (hiTail != null) {  
    65.                         hiTail.next = null;  
    66.                         newTab[j + oldCap] = hiHead;  
    67.                     }  
    68.                 }  
    69.             }  
    70.         }  
    71.     }  
    72.     return newTab;  
    73. }  

    从第15 ~ 20行可以看到,若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16
     
    当table需要扩容时,从第11 ~ 13行可以看到,扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:
    假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定
    那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
    因此,table中的元素只有两种情况:
    1. 元素hash值第N+1位为0:不需要进行位置调整
    2. 元素hash值第N+1位为1:调整至原索引的两倍位置
    在resize方法中,第45行的判断即用于确定元素hashi值第N+1位是否为0:
    • 若为0,则使用loHead与loTail,将元素移至新table的原索引处
    • 若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处
    扩容或初始化完成后,resize方法返回新的table
  • 相关阅读:
    root登录出现“sorry, that didn't work please try again”
    【自适应辛普森】积分计算
    【CF1553F】Pairwise Modulo
    调和级数的复杂度
    CF 1600-2000 的思维题
    中超热身赛(2021湘潭全国邀请赛-重现)补题
    牛客2021年度训练联盟热身训练赛第一场(讲题)
    新知识-Queue_循环队列
    新知识-valueOf(Leetcode 1556_千位分隔符)
    新知识-位运算(Leetcode 217_存在重复元素)
  • 原文地址:https://www.cnblogs.com/KingIceMou/p/6976574.html
Copyright © 2011-2022 走看看