爲啥HashMap的長度必定是2的n次方

前言


個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關,劍指offer題解(Java版),能夠點個star。能夠看個人github主頁,天天都在更新喲。java

邀請您跟我一同完成 repogit


謹記

首先你應當記住的:無論你傳不傳參數,無論你傳入的長度爲多少,在你用HashMap的時候,他的長度都是2的n次方,且最大長度爲2的30次方github

最大長度

在HashMap的源碼中,最大長度這個常量值是這樣定義的算法

/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
    static final int MAXIMUM_CAPACITY = 1 << 30;
複製代碼

這個值用在哪裏呢?spring

  • resize()函數,這個是用來擴容的
  • tableSizeFor(),這個也是用來擴容的
  • 構造函數中
  • putEntries(),存放一組HashMap元素時,不是存放單個

爲何table長度必定是2的n次方

注意,源碼中他們採用了延遲初始化操做,也就是table只有在用到的時候才初始化,若是你不對他進行put等操做的話,table的長度永遠爲"零"數組

主要有兩個函數保證了他的長度爲2的n次方函數

  • tableSizeFor()
  • resize()

至於計算過程以及加載過程,請參考個人這篇文章:table的長度究竟是多少源碼分析

這篇文章我從源碼分析table的建立過程,包括上面提到的函數的調用,看了這個你必定明白爲啥table的長度必定是2的n次方spa

固然我針對hashMap寫的一部分源碼的中文註釋github上也有:HashMap源碼中文註釋.net

2的n次有什麼好處

  • 計算方便
  • hash分佈更均勻

分佈均勻

若是不是2的n次方,那麼有些位置上是永遠不會被用到

具體能夠參考這篇博文,他用例子講述了爲何,爲啥長度要是2的n次方

計算方便

  • 當容量必定是2^n時,h & (length - 1) == h % length

  • 擴容後計算新位置,很是方便,相比 JDK1.7

JDK 1.8改動

在 JDK1.8 中,HashMap有了挺大的改動,包括

  • 元素遷移算法(舊的到新的數組)

  • 使用紅黑樹

  • 鏈表爲尾插法

其中我重點講下元素遷移算法,JDK1.8的時候

首先看下java代碼以及個人註釋,若是要看完整的,能夠看個人github倉庫

// 將原來數組中的全部元素都 copy進新的數組
if(oldTab != null){
    for (int j = 0; j < oldCap; j++) {
        Entry e;

        if((e = oldTab[j]) != null){
            oldTab[j] = null;

            // 說明尚未成鏈,數組上只有一個
            if(e.next == null){
                // 從新計算 數組索引 值
                newTable[e.h & (newCap-1)] = e;

            }
            // 判斷是否爲樹結構
            //else if (e instanceof TreeNode)


            // 若是不是樹,只是鏈表,即長度尚未大於 8 進化成樹
            else{
                // 擴容後,若是元素的 index 仍是原來的。就使用這個lo前綴的
                Entry loHead=null, loTail =null;

                // 擴容後 元素index改變,那麼就使用 hi前綴開頭的
                Entry hiHead = null, hiTail = null;
                Entry next;
                do {
                    next = e.next;
                    //這個很是重要,也比較難懂,
                    // 將它和原來的長度進行相與,就是判斷他的原來的hash的上一個 bit 位是否爲 1。
                    //以此來判斷他是在相同的索引仍是table長度加上原來的索引
                    if((e.h & oldCap) == 0){
                        // 若是 loTail == null ,說明這個 位置上是第一次添加,沒有哈希衝突
                        if(loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else{
                        if(hiTail == null)
                            loHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e ;
                    }

                }while ((e = next) != null);


                if(loTail != null){
                    loTail.next = null;
                    newTable[j] = loHead;
                }

                // 新的index 等於原來的 index+oldCap
                else {

                    hiTail.next = null;
                    newTable[j+oldCap] = hiHead;
                }

            }
        }

    }
}
複製代碼

咱們看到上面源碼的最後一句,newTable[j+oldCap] = hiHead;意思就是哪怕咱們的元素從舊的數組遷移到新的數組,咱們也不須要從新計算他的hash和新數組長度相與的值,只須要直接將如今的索引值+原來數組的長度便可

藍色的表示不須要移動的,綠色的表示須要從新計算索引的,咱們看到,他只是加了16(原來的數組table長度)

計算索引須要

咱們注意到上面的源代碼中,判斷擴容後元素位置需不須要改變的時候,咱們使用到了這個判斷

if((e.h & oldCap) == 0)

若是爲0,那麼就不須要改變,使用舊的索引便可;若是爲1,那麼就須要使用新的索引

爲啥會這樣呢?

  • 若是元素的索引要變那麼 hash&(newTable.length-1)必定是和 hash&(oldTable.length-1)+oldTable.length相等
  • 由於table的長度必定是2的n次方,也就是oldCap 必定是2的n次方,也就是說 oldCap有且只有一位是1,並且這個位置在最高位;

咱們來舉個例子:

咱們假設元素的hash值的後12位是 110111010111,數組原來的長度爲16,擴容後數組長度爲32

你能夠試下下次擴容時,擴容到64時,索引變不變化。固然答案是不會變化,由於元素的hash值在那個位置爲 0

對比1.7擴容

咱們來對比JDK1.7 的方式,他若是要擴容,而且擴容後計算元素的索引的話要使用 indexFor函數

/** * Returns index for hash code h. */  
    static int indexFor(int h, int length) {  
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; 
        return h & (length-1);  
    }  
複製代碼

也就是要把元素的hash值從新再和新的數組長度-1 再相與一次,會比較麻煩並且不優雅,徹底沒有我看到1.8計算方式的那種驚豔感。

相關文章
相關標籤/搜索