java7和java8 hashmap擴容機制及區別

轉自:http://www.cnblogs.com/yanzige/p/8392142.html

(一) Java 7 中Hashmap擴容機制

一、什麼時候擴容:

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

擴容必須滿足兩個條件:

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個了,則會將該樹轉爲鏈表。

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章