看了網上很多文章,說HashMap在元素達到負載因子對應數的時候就發生擴容。如果你看過源碼就會發現,其實還有一種情況也可能會發生擴容:樹形化的時候。
對象最終是如何放入HashMap中的?
HashMap底層是由數組+鏈表組成的,為了方便不懂的人更容易理解,那我們就先假設HashMap底層就是數組,先不管鏈表。
當一個對象add到HashMap中,此時HashMap的add方法是如何來確定這個對象是放在數組中的哪個位置的呢?
拿JDK1.8來說(其他JDK版本稍有不同,但大同小異),大家應該知道每一個對象天生都繼承了或程序員自己覆蓋了Object類的 hashCode()方法,此方法返回對象的hashcode值。
HashMap會有一個方法,先拿到要add進HashMap中的對象的hashCode,再將這個hashCode異或上對象自身hashCode右移16位(是不是感覺說的不是人話?這個步驟叫擾亂,這樣做的目的是為了讓hashCode每一位都盡可能用到,如果不理解沒關系並不影響接下來的閱讀),hashCode經過上述步驟之后再&(數組長度-1),計算的結果就是這個對象在數組中的位置了。我自己都覺得說的不是人話,下面舉個例子,便於理解:
這里有一個Student對象的hashCode是:a
先把這個a右移16位 , b=a>>>16;
然后a=a&b;
數組中的位置等於: a&(數組長度-1);
上述源碼如下:
h=key.hashCode();
h = key.hashCode()) ^ (h >>> 16)
數組位置=h&(數組長度-1);
- 1
- 2
- 3
好了, 我們已經知道元素是如何在hashMap中的數組上如何定位了,現在假設一個極端情況(不可能發生,但是我用這個舉例子):
假設數組長度為1,根據源碼:
數組位置=h&(數組長度-1)
那么有:
數組位置=h&(1-1)=0 ,無論什么對象,都定位到數組的第0個位置。
這個很好理解吧。無論元素是否一樣,由於數組長度為1,所以元素通通定位到數組中第0個位置。大家都知道一個數組只能放一個元素啊?那怎么辦呢?我們用鏈表來解決這個問題,把定位到這個位置的元素通過鏈表連接。這就是我一開始說的:hashMap是數組+鏈表。
那樹形化又是什么東東呢?
想一下我們為什么要用HashMap,是因為通過Hash算法在理想情況下時間復雜度O(1)就能找到元素,特別快,但是我都說了是理想情況,如果遇到上述發生hash碰撞(誰jb取的名字,就是上面我才說的,兩個元素定位到數組中同一個位置),且hash碰撞比較頻繁的話,那么當我們get一個元素的時候,定位到了這個數組,還需要在數組中遍歷一次鏈表最終才能找到要get的元素,是不是已經失去一部分使用HashMap的初心了?(因為需要遍歷鏈表,所以時間復雜度就比之前高了)
所以JDK1.8使用紅黑樹這種數據結構來解決鏈表過長的問題(可以簡單理解為用紅黑樹遍歷比鏈表遍歷速度快,時間復雜度低,不懂紅黑樹的可以去搜搜看),默認鏈表長度達到8就將鏈表樹形化(變為紅黑樹)。
回到最最開始我提到的,那為什么樹形化的時候可能會發生擴容呢?
想想剛剛的例子數組長度為1,所有元素全部在數組的第0個位置形成一條鏈表,這例子是一種極端情況,數組長度過小,那自然就會經常發生hash碰撞,那形成長鏈表是肯定的,這個時候樹形化其實是治標不治本,因為引起鏈表過長的根本原因是數組過短,所以在JDK1.8源碼中,執行樹形化之前,會先檢查數組長度,如果長度小於64,則對數組進行擴容,而不是進行樹形化。
所以發生擴容的時候有兩種情況,一種是元素達到閥值了,一種是HashMap准備樹形化但又發現數組太短,這兩種情況均可能發生擴容。