深入理解HashMap的擴容機制


                                 Java 7 中Hashmap擴容機制

原文鏈接:https://www.cnblogs.com/yanzige/p/8392142.html

一、什么時候擴容:

網上總結的會有很多,但大多都總結的不夠完整或者不夠准確。大多數可能值說了滿足我下面條件一的情況。

擴容必須滿足兩個條件:

1、 存放新值的時候當前已有元素的個數必須大於等於閾值

2、 存放新值的時候當前存放數據發生hash碰撞(當前key計算的hash值換算出來的數組下標位置已經存在值)

 

二、下面我們看源碼,如下:

首先是put()方法

public  V put(K key, V value) {
     //判斷當前Hashmap(底層是Entry數組)是否存值(是否為空數組)
     if  (table == EMPTY_TABLE) {
      inflateTable(threshold); //如果為空,則初始化
    }
    
     //判斷key是否為空
     if  (key == null )
       return  putForNullKey(value); //hashmap允許key為空
    
     //計算當前key的哈希值    
     int  hash = hash(key);
     //通過哈希值和當前數據長度,算出當前key值對應在數組中的存放位置
     int  i = indexFor(hash, table.length);
     for  (Entry<K,V> e = table[i]; e != null ; e = e.next) {
      Object k;
       //如果計算的哈希位置有值(及hash沖突),且key值一樣,則覆蓋原值value,並返回原值value
       if  (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess( this );
         return  oldValue;
      }
    }
 
    modCount++;
     //存放值的具體方法
    addEntry(hash, key, value, i);
     return  null ;
  }

   

在put()方法中有調用addEntry()方法,這個方法里面是具體的存值,在存值之前還要判斷是否需要擴容

void  addEntry( int  hash, K key, V value, int  bucketIndex) {
     //1、判斷當前個數是否大於等於閾值
     //2、當前存放是否發生哈希碰撞
     //如果上面兩個條件否發生,那么就擴容
     if  ((size >= threshold) && ( null  != table[bucketIndex])) {
       //擴容,並且把原來數組中的元素重新放到新數組中
      resize( 2  * table.length);
      hash = ( null  != key) ? hash(key) : 0 ;
      bucketIndex = indexFor(hash, table.length);
    }
 
    createEntry(hash, key, value, bucketIndex);
  }

  

如果需要擴容,調用擴容的方法resize()

void  resize( int  newCapacity) {
    Entry[] oldTable = table;
     int  oldCapacity = oldTable.length;
     //判斷是否有超出擴容的最大值,如果達到最大值則不進行擴容操作
     if  (oldCapacity == MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
       return ;
    }
 
    Entry[] newTable = new  Entry[newCapacity];
     // transfer()方法把原數組中的值放到新數組中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
     //設置hashmap擴容后為新的數組引用
    table = newTable;
     //設置hashmap擴容新的閾值
    threshold = ( int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 );
  }

 

transfer()在實際擴容時候把原來數組中的元素放入新的數組中

void  transfer(Entry[] newTable, boolean  rehash) {
     int  newCapacity = newTable.length;
     for  (Entry<K,V> e : table) {
       while ( null  != e) {
        Entry<K,V> next = e.next;
         if  (rehash) {
          e.hash = null  == e.key ? 0  : hash(e.key);
        }
         //通過key值的hash值和新數組的大小算出在當前數組中的存放位置
         int  i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

  

三、總結:

Hashmap的擴容需要滿足兩個條件:當前數據存儲的數量(即size())大小必須大於等於閾值;當前加入的數據是否發生了hash沖突。

 

因為上面這兩個條件,所以存在下面這些情況

(1)、就是hashmap在存值的時候(默認大小為16,負載因子0.75,閾值12),可能達到最后存滿16個值的時候,再存入第17個值才會發生擴容現象,因為前16個值,每個值在底層數組中分別占據一個位置,並沒有發生hash碰撞。

(2)、當然也有可能存儲更多值(超多16個值,最多可以存26個值)都還沒有擴容。原理:前11個值全部hash碰撞,存到數組的同一個位置(雖然hash沖突,但是這時元素個數小於閾值12,並沒有同時滿足擴容的兩個條件。所以不會擴容),后面所有存入的15個值全部分散到數組剩下的15個位置(這時元素個數大於等於閾值,但是每次存入的元素並沒有發生hash碰撞,也沒有同時滿足擴容的兩個條件,所以葉不會擴容),前面11+15=26,所以在存入第27個值的時候才同時滿足上面兩個條件,這時候才會發生擴容現象。

 

 

 

                                                                                            Java 8 中Hashmap擴容機制

一、Java8的擴容機制:

  Java8不再像Java7中那樣需要滿足兩個條件,Java8中擴容只需要滿足一個條件:當前存放新值(注意不是替換已有元素位置時)的時候已有元素的個數大於等於閾值(已有元素等於閾值,下一個存放后必然觸發擴容機制)

  注:

  (1)擴容一定是放入新值的時候,該新值不是替換以前位置的情況下(說明:put(“name”,"zhangsan"),而map里面原有數據<"name","lisi">,則該存放過程就是替換一個原有值,而不是新增值,則不會擴容)

  (2)擴容發生在存放后,即是數據存放后(先存放后擴容),判斷當前存入對象的個數,如果大於閾值則進行擴容。

 

二、背靜知識:

  Java7中Hashmap底層采用的是Entry對數組,而每一個Entry對又向下延伸是一個鏈表,在鏈表上的每一個Entry對不僅存儲着自己的key/value值,還存了前一個和后一個Entry對的地址。

  Java8中的Hashmap底層結構有一定的變化,還是使用的數組,但是數組的對象以前是Entry對,現在換成了Node對象(可以理解是Entry對,結構一樣,存儲時也會存key/value鍵值對、前一個和后一個Node的地址),以前所有的Entry向下延伸都是鏈表,Java8變成鏈表和紅黑樹的組合,數據少量存入的時候優先還是鏈表,當鏈表長度大於8,且總數據量大於64的時候,鏈表就會轉化成紅黑樹,所以你會看到Java8的Hashmap的數據存儲是鏈表+紅黑樹的組合,如果數據量小於64則只有鏈表,如果數據量大於64,且某一個數組下標數據量大於8,那么該處即為紅黑樹。

 

三、源碼:

  在jdk7中,當new Hashmap()的時候會對對象進行初始化,而jdk8中new Hashmap()並沒有對對象進行初始化,而是在put()方法中通過判斷對象是否為空,如果為空通過調用resize()來初始化對象。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
復制代碼
    /**
     * Implements Map.put and related methods
     *
     * @param hash key值計算傳來的下標
     * @param key
     * @param value
     * @param onlyIfAbsent true只是在值為空的時候存儲數據,false都存儲數據
     * @param evict
     * @return 返回被覆蓋的值,如果沒有覆蓋則返回null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 申明entry數組對象tab[]:當前Entry[]對象
        Node<K,V>[] tab;
        // 申明entry對象p:這里表示存放的單個節點
        Node<K,V> p;
        // n:為當前Entry對象長度 
     // i:為當前存放對象節點的位置下標 int n, i; /** * 流程判斷 * 1、如果當前Node數組(tab)為空,則直接創建(通過resize()創建),並將當前創建后的長度設置給n * 2、如果要存放對象所在位置的Node節點為空,則直接將對象存放位置創建新Node,並將值直接存入 * 3、存放的Node數組不為空,且存放的下標節點Node不為空(該Node節點為鏈表的首節點) * 1)比較鏈表的首節點存放的對象和當前存放對象是否為同一個對象,如果是則直接覆蓋並將原來的值返回 * 2)如果不是分兩種情況 * (1)存儲處節點為紅黑樹node結構,調用方法putTreeVal()直接將數據插入 * (2)不是紅黑樹,則表示為鏈表,則進行遍歷 * A.如果存入的鏈表下一個位置為空,則先將值直接存入,存入后檢查當前存入位置是否已經大於鏈表的第8個位置 * a.如果大於,調用treeifyBin方法判斷是擴容 還是 需要將該鏈表轉紅黑樹(大於8且總數據量大於64則轉紅黑色,否則對數組進行擴容) * b.當前存入位置鏈表長度沒有大於8,則存入成功,終端循環操作。 * B.如果存入鏈表的下一個位置有值,且該值和存入對象“一樣”,則直接覆蓋,並將原來的值返回 * 上面AB兩種情況執行完成后,判斷返回的原對象是否為空,如果不為空,則將原對象的原始value返回 * 上面123三種情況下,如果沒有覆蓋原值,則表示新增存入數據,存儲數據完成后,size+1,然后判斷當前數據量是否大於閾值, * 如果大於閾值,則進行擴容。 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 按照紅黑樹直接將數據存入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); 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; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果不是替換數據存入,而是新增位置存入后,則將map的size進行加1,然后判斷容量是否超過閾值,超過則擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
復制代碼

 

  treeifyBin()方法判斷是擴容還是將當前鏈表轉紅黑樹
復制代碼
    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     * 從指定hash位置處的鏈表nodes頭部開始,全部替換成紅黑樹結構。
     * 除非整個數組對象(Map集合)數據量很小(小於64),該情況下則通過resize()對這個Map進行擴容,而代替將鏈表轉紅黑樹的操作。
     */
    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        int n, index; HashMap.Node<K,V> e;
        // 如果Map為空或者當前存入數據n(可以理解為map的size())的數量小於64便進行擴容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 如果size()大於64則將正在存入的該值所在鏈表轉化成紅黑樹
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            HashMap.TreeNode<K,V> hd = null, tl = null;
            do {
                HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
復制代碼

 

四、總結:

  (1)Java 8 在新增數據存入成功后進行擴容

  (2)擴容會發生在兩種情況下(滿足任意一種條件即發生擴容):

      a 當前存入數據大於閾值即發生擴容

      b 存入數據到某一條鏈表上,此時數據大於8,且總數量小於64即發生擴容

  (3)此外需要注意一點java7是在存入數據前進行判斷是否擴容,而java8是在存入數據庫在進行擴容的判斷。

 

ConcurrentHashMap知識參考:https://www.cnblogs.com/zerotomax/p/8687425.html

Java8 HashMap擴容可參考:https://blog.csdn.net/goosson/article/details/81029729 (注:該文章中關於Java8 底層數據結構描述不准確,只有當數據量大於64才會有紅黑樹+鏈表)

這里補充一下jdk8關於紅黑樹和鏈表的知識:

  第一次添加元素的時候,默認初期長度為16,當往map中繼續添加元素的時候,通過hash值跟數組長度取“與”來決定放在數組的哪個位置,如果出現放在同一個位置的時候,優先以鏈表的形式存放,在同一個位置的個數又達到了8個(代碼是>=7,從0開始,及第8個開始判斷是否轉化成紅黑樹),如果數組的長度還小於64的時候,則會擴容數組。如果數組的長度大於等於64的話,才會將該節點的鏈表轉換成樹。在擴容完成之后,如果某個節點的是樹,同時現在該節點的個數又小於等於6個了,則會將該樹轉為鏈表。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM