hashmap的擴容機制是什麼

2023-03-15 18:01:05

hashmap的擴容機制是:重新計算容量,用一個新的陣列替換原來的陣列。重新計算原陣列的所有資料並插入一個新陣列,然後指向新陣列;如果陣列在容量擴充套件前已達到最大值,則直接將閾值設定為最大整數返回。

本教學操作環境:windows7系統、java8、Dell G3電腦。

什麼是擴容(resize)?

 擴容(resize):就是重新計算容量,向HashMap物件裡不停的新增元素,而HashMap物件內部的陣列無法裝載更多的元素時,物件就需要擴大陣列的長度,以便能裝入更多的元素。當然Java裡的陣列是無法自動擴容的,方法是使用一個新的陣列代替已有的容量小的陣列,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

什麼時候擴容?

 當向容器新增元素的時候,會判斷當前容器的元素個數,如果大於等於閾值(threshold),即當前容器內的元素個數大於當前陣列的長度乘以載入因子的值的時候,就要自動擴容了。

hashmap擴容原理

hashMap擴容就是重新計算容量,向hashMap不停的新增元素,當hashMap無法裝載新的元素,物件將需要擴大陣列容量,以便裝入更多的元素。

1.jpg

HashMap容量擴充套件的特性,載入因子越大,空間利用率越高,擴容前需要填充的元素越多,put操作越快,但連結串列容易過長,hash碰撞概率大,get操作慢。載入因子越小,get操作越快,連結串列越短,hash碰撞概率低。但是,空間利用率低。put元素太多會導致頻繁擴容,影響效能。

2.jpg

HashMap的容量擴充套件原理:Hashmap的方法是用新陣列替換原陣列,重新計算原陣列中的所有資料,插入新陣列,然後指向新陣列;如果陣列在擴容前已經達到最大,則直接將閾值設定為最大整數返回。

擴容的過程

 下面採用原始碼+圖片+文字描述的方式介紹HashMap的擴容過程。

/** 
 * HashMap 新增節點 
 * 
 * @param hash        當前key生成的hashcode 
 * @param key         要新增到 HashMap 的key 
 * @param value       要新增到 HashMap 的value 
 * @param bucketIndex 桶,也就是這個要新增 HashMap 裡的這個資料對應到陣列的位置下標 
 */  
void addEntry(int hash, K key, V value, int bucketIndex) {  
    //陣列擴容條件:1.已經存在的key-value mappings的個數大於等於閾值  
    //             2.底層陣列的bucketIndex座標處不等於null  
    if ((size >= threshold) && (null != table[bucketIndex])) {  
        resize(2 * table.length);//擴容之後,陣列長度變了  
        hash = (null != key) ? hash(key) : 0;//為什麼要再次計算一下hash值呢?  
        bucketIndex = indexFor(hash, table.length);//擴容之後,陣列長度變了,在陣列的下標跟陣列長度有關,得重算。  
    }  
    createEntry(hash, key, value, bucketIndex);  
}  
  
/** 
 * 這地方就是連結串列出現的地方,有2種情況 
 * 1,原來的桶bucketIndex處是沒值的,那麼就不會有連結串列出來啦 
 * 2,原來這地方有值,那麼根據Entry的建構函式,把新傳進來的key-value mapping放在陣列上,原來的就掛在這個新來的next屬性上了 
 */  
void createEntry(int hash, K key, V value, int bucketIndex) {  
    HashMap.Entry<K, V> e = table[bucketIndex];  
    table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);  
    size++;  
}
登入後複製

 上述addEntry方法中,如果size(當前容器內的元素個數)大於等於threshold(陣列長度乘以負載因子),並且底層陣列的bucketIndex座標處不等於null,那麼將會進行擴容(resize)。否則,不會進行擴容。

 下面將著重介紹一下擴容的過程:

        void resize(int newCapacity) {   //傳入新的容量
            Entry[] oldTable = table;    //參照擴容前的Entry陣列
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的陣列大小如果已經達到最大(2^30)了
                threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以後就不會擴容了
                return;
            }
     
            Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry陣列
            transfer(newTable);	此行有遺漏,勘誤見下面參照	//!!將資料轉移到新的Entry陣列裡
            table = newTable;                           //HashMap的table屬性參照新的Entry陣列
            threshold = (int) (newCapacity * loadFactor);此行有遺漏,勘誤見下面參照//修改閾值
        }
登入後複製

由wenni328博友修正:transfer(newTable); ==> transfer(newTable, initHashSeedAsNeeded(newCapacity));
threshold = (int) (newCapacity * loadFactor); ==> threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

 擴容前首先獲取擴容前陣列的參照地址存進oldTable變數中,然後判斷擴容前陣列長度是否達到了int型別儲存的最大值,如果是則放棄此次擴容,因為陣列容量已經達到最大,無法再擴容了。

 下圖為程式執行完Entry[] newTable = new Entry[newCapacity];程式碼之後的狀態:

3.png

 這裡就是使用一個容量更大的陣列來代替已有的容量小的陣列,transfer()方法將原有Entry陣列的元素拷貝到新的Entry陣列裡。

        void transfer(Entry[] newTable) {
            Entry[] src = table;                   //src參照了舊的Entry陣列
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) { //遍歷舊的Entry陣列
                Entry<K, V> e = src[j];             //取得舊Entry陣列的每個元素
                if (e != null) {
                    src[j] = null;//釋放舊Entry陣列的物件參照(for迴圈後,舊的Entry陣列不再參照任何物件)
                    do {
                        Entry<K, V> next = e.next;
                        int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在陣列中的位置
                        e.next = newTable[i]; //標記[1]
                        newTable[i] = e;      //將元素放在陣列上
                        e = next;             //存取下一個Entry鏈上的元素
                    } while (e != null);
                }
            }
        }

        static int indexFor(int h, int length) {
            return h & (length - 1);
        }
登入後複製

 newTable[i]的參照賦給了e.next,也就是使用了單連結串列的頭插入方式,同一位置上新元素總會被放在連結串列的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發生了hash衝突的話)。在舊陣列中同一條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新陣列的不同位置上。

 下面會以圖片的形式演示一下transfer的過程(下面圖片的紅色字型表示與上圖有區別的地方,後面圖片都是這樣,後面紅色字型說明不再贅述)

 下圖為程式執行完src[j] = null;程式碼之後的狀態(這是第一次迴圈時的狀態):

4.png

 首先,將table[]陣列的參照地址賦值給src[]陣列。

 然後,Entry<K, V> e = src[j];是將src[j]位置的連結串列交給e變數儲存。由於src[j]位置的連結串列已經交給e儲存了,所以可以大膽的將src[j]=null;然後等待垃圾回收。

 下圖為程式執行完Entry<K, V> next = e.next;程式碼之後的狀態(這是第一次迴圈時的狀態):

5.png

 這裡先將e.next的值備份至next變數中,後續程式碼會將e.next的指向更改,所以在這裡將e.next的值備份。

 下圖為程式執行完e.next = newTable[i];程式碼之後的狀態(這是第一次迴圈時的狀態):

6.png

 由於newTable[3]的值為null,所以e.next為null,如上圖所示。

 下圖為程式執行完newTable[i] = e;程式碼之後的狀態(這是第一次迴圈時的狀態):

7.png

 下圖為程式執行完e = next;程式碼之後的狀態(這是第一次迴圈時的狀態):

8.png

 如上述所示,Entry1這個節點成功插入到了newTable中,一輪迴圈結束時,因為判斷e!=null,所以會再重複上述過程,直至所有節點移動到newTable中。

小結

  • 擴容是一個特別耗效能的操作,所以當程式設計師在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
  • 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。
  • HashMap是執行緒不安全的,不要在並行的環境中同時操作HashMap,建議使用ConcurrentHashMap。
  • JDK1.8引入紅黑樹大程度優化了HashMap的效能。

更多程式設計相關知識,請存取:!!

以上就是hashmap的擴容機制是什麼的詳細內容,更多請關注TW511.COM其它相關文章!