如果HashMap的容量不是2次冪,會發生什麼?

絮叨

HashMap作爲Java中最常用的數據結構之一,在工作中使用HashMap的頻率和你遇見NullPointException一樣多,在面試中被問到的概率和問你名字的概率一樣大。既然工作,面試經常遇到,我們有必要熟悉HashMap的每一個細節。作爲最常用的數據結構之一,我們都知道HashMap的容量爲2次冪。當被問到爲什麼是2次冪時,大家應該都能回答出來,是爲了均勻分散到數組中。但是2次冪爲什麼就能均勻分散,3次冪不能均勻分散嗎?如果你一頭霧水,看了這篇文章你就會明白爲什麼只有HashMap的容量爲2次冪能夠使鍵值對均勻分散。

說明一下文章中所有代碼來源於JDK1.8

正文

假設HashMap的初始容量爲17

我們來看看HashMap是如何存數據的。

HashMap底層數據結構爲數組+鏈表+紅黑樹。當我們向HashMap中插入一個對象,HashMap需要知道我這個對象要插入到數組的哪個位置。當得到插入到數組的下標index後,HashMap還需要去分辨這個位置有沒有值。如果此時沒有值,廢話不多話直接往這個位置放值。但是如果當前位置不是空怎麼辦,已經有一個對象在那呢,這很簡單,覆蓋就行了。可是如果有個鏈表佔着呢,鏈表?鏈表也沒事,遍歷這個鏈表,放到鏈表的尾部去。HashMap大哥!這個位置如果被紅黑樹佔着呢!紅黑樹啊,讓紅黑樹乖乖的給我找個位置放這個對象,改改顏色,調整一下樹結構,這對紅黑樹來說這麼簡單的事。至此一個結點被放到一個正確的位置。

我們來好好看看由key映射到index的算法。

int index = (n - 1) & hash

HashMap大哥!這個映射算法爲什麼要這麼寫啊?爲什麼不用%啊?

這個給你好好說道說道,拿小本本記下來!

1:&運算比%運算效率高很多
2:當n爲2次冪,(n-1)&hash等同於hash%n

嗷嗷,原來是這樣子!

那接下來我們來看一下當前HashMap的初始容量爲17,即n等於17時,會發生什麼。

1.n=17則n-1=16,16的二進制表示爲0010000。
2.如果我現在向HashMap中插入兩個鍵值對,key1=A,Key2=B
  假設通過Hash算法得知A的hash值爲1010110,B的Hash值爲0110000
3.分別算出key等於A,B時所映射的index
  0010000  0010000
  1010110  0110000

———————————

  0010000  0010000

HashMap大大!爲什麼Hash值差別那麼大,得到的index值確實相同

這是由於&運算的特殊性導致的,在二進制中&符號兩邊的元素只要有一個爲0,"&"操作執行後的結果就爲0。

其實當n=17時,計算出來的index只有兩個值,0或者16
如果我們向這個HashMap中保存大量的鍵值對,所有的鍵值對都會堆積在數據的第0個或者第16個位置,而第1-15個位置一直是null。

可是爲什麼當n爲二次冪時,可以讓鍵值對均勻分散,不會造成鍵值對堆積呢?

這個問題我反問你一下,當n爲二次冪,n-1的二進制表示是什麼樣子?

當n爲二次冪,n-1的二進制表示中所有的低位均爲1,如果n是16,n-1的二進制表示就是01111。這個時候通過(n-1)&hash得到的值能夠分散到0-15的每一個位置中,而且分散到每一個位置的概率都相同,不會發生鍵值對堆積在某幾個位置的現象。

講到這裏大家對HashMap的容量爲什麼是二次冪應該有了瞭解,爲了讓鍵值對均勻分散到數組中。如果鍵值對只是堆積在某幾個位置,這完全浪費了數組其餘位置的資源,同時還增加了HashMap獲取鍵值對的複雜度,對cpu資源也是一種浪費。

接下來的內容是對Hash算法和HashMap數組容量的引申內容,但是也是屬於HashMap中必知必會的知識點,一定要喫透理解透。

說到Hash算法,我們來思考這一個問題,如果有很多key的hash值低位不變化,只是在高位變化,這樣豈不是也會造成鍵值對的堆積。比如010110000,110110000,000010000。如果此時n等於32,計算出來的index都等於16。

這個問題在設計Hash算法的時候就想到啦,來看看我的hash值怎麼計算出來的你就明白啦。

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

仔細看看註釋,你這個問題在註釋裏就能找到啦。我來簡單的翻譯一下吧(英語有待加強)。

計算key的hashcode值,同時將hash值的高位傳遞給低位。因爲表的長度使用二次冪,因此僅在高位變化的散列總是發生衝突(已知的例子是在小的表中保存連續整數的浮點鍵)。所有我們就利用了一個轉換,將高位向下轉移。這是一個在速度,效率,位移動上的交易。因爲許多的Hash值是合理分佈的(所有不利於傳遞)。並且我們使用紅黑樹去處理更多hash值的碰撞情況。所有我們用一些減少系統消耗並且廉價的方式去轉移一些字節,以及可以利用高位的值來計算index值,否則高位不會參與到index的計算中。

HashMap大大!我明白了!爲了避免這種Hash值低位不變,僅高位變化的情況,所有HashMap的設計者在設計Hash算法時利用“^”運算使得Hash值的高位與低位相互碰撞,讓Hash值的高位與低位同時參與index的計算中。HashMap大大!還有一個問題,在HashMap的構造方法中我可以傳入容量這個參數啊,如果我傳入的值是非二次冪,那豈不是會造成鍵值對堆積的情況。

你還是太年輕了,雖然你傳進的容量值是一個非二次冪,但是初始化的時候HashMap真正的容量值可是和你傳入的值不一樣哦。來,仔細看看HashMap這個構造方法。

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

情況1.如果傳入的initialCapacity小於0,拋異常。

情況2.如果initialCapacity大於最大容量,將最大容量賦予給initialCapacity。

最後3.調用tableSizeFor方法,將initialCapacity轉化爲一個二次冪的值,然後賦予給threshold。

Tip:HashMap初始化時,使用threshold作爲HashMap數組的長度。

HashMap大大!如果我通過HashMap的構造函數傳入的容量是一個非二次冪,HashMap會通過tableSizeFor方法將這個值轉化爲二次冪,這麼理解對嗎?

對,總結很到位,我們來看看tableSizeFor方法如何將一個非二次冪轉化爲一個二次冪

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

這部分有點難理解,舉個例子說明

假設傳入的參數cap是一個非二次冪,cap=17

1.n-1=16。二次冪表示爲0010000(一個1)

2.n |= n >>> 1。計算得到n等於0011000(兩個1)

3.n |= n >>> 2。計算得到n等於0011110(四個1)

4.n |= n >>> 4。計算得到n等於0011111(五個一)

5.n |= n >>> 8。計算得到n等於0011111(五個一)

6.n |= n >>> 16。計算得到n等於0011111(五個一)

7.n+1等於0100000,十進制等於32。

好精妙的算法啊!

哈哈,學無止境,現在到我們的總結時間了

總結

1.當HashMap的數組長度爲二次冪,能夠將鍵值對均勻分散到數組的每一個位置上。

2.index映射算法和%能夠達成一樣的效果,並且效率更高效。

3.Hash算法在設計時利用移位和異或運算符“^”來避免Hash碰撞。

4.如果通過HashMap的構造函數傳入的容量是一個非二次冪,HashMap會通過tableSizeFor方法將這個值轉化爲二次冪的值。

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