1.考考你
這是咱們高級併發編程系列的第十五篇,這一篇原來是準備寫ConcurrentHashMap,標題我都想好了,叫作:一文搞懂ConcurrentHashMap。java
可是想了想,要說清楚ConcurrentHashMap,還須要優先說清楚HashMap,因而咱們臨時把標題換一換,換成一文搞懂HashMap。不過你放心,關於ConcurrentHashMap,我會放到下一篇來分享。編程
相信這兩篇文章,你都會有所收穫。我打算從分析底層原理,結合源碼的方式,爭取讓咱們在實際項目開發中,用到HashMap,或者ConcurrentHashMap的時候,都可以知其然,還知其因此然。那麼讓咱們開始吧數組
#考考你: 1.不少時候,你都會直接用到HashMap,那麼你真的認識HashMap嗎 2.關於HashMap,你知道什麼是hash衝突,負載因子,以及如何擴容嗎
2.案例
2.1.HashMap原理
2.1.1.原理分析
提及HashMap的底層實現原理,用到的數據結構,你必定很熟系:數組。咱們一塊兒來嘗試分析一下,爲何HashMap的底層數據結構會選擇數組呢?緩存
你能夠先回顧一下,數組這種數據結構都有什麼特色。咱們一塊兒來回憶一下:數組是一種基於線性表的數據結構,支持按照下標隨機訪問,時間複雜度是O(1),很是高效。bash
你再回顧一下,日常在項目中使用HashMap,相應的業務場景有哪些?一般狀況下,咱們把HashMap做爲一個容器,好比說本地緩存解決方案,把緩存目標對象,按照key/value的方式存放到HashMap中,須要用到緩存對象的時候,根據緩存key從容器中查找目標對象。數據結構
這裏你須要注意兩個字:查找。等價於說咱們使用HashMap,大多數業務場景都是在讀多寫少的場景(一次寫入,屢次讀取)。對於讀咱們的指望是要高效,最好是O(1)的時間複雜度,嗯這不數組自然就知足嗎?併發
到這裏,我相信你應該可以理解了:爲何HashMap的底層數據結構,會選擇數組。我給你看一個對應關係圖,就不會那麼抽象了:函數
2.1.2.一圖勝千言,圖解
上圖便是HashMap的底層實現,你須要關注:左邊的散列函數、中間的數組、右邊的鏈表。咱們來逐個分析一下。源碼分析
咱們已經明確了HashMap的底層數據結構是數組,即HashMap中的元素,其實就是存在數組中,所以中間的數組,對於你來講,理解起來不是什麼難事。spa
這裏有一個問題,咱們知道數組是按照下標來查找數組中元素的,即下標0,1,2......好比array[0]=小明。實際使用HashMap中,咱們是經過key/value鍵值對的方式,與之對應array[0]是HashMap中key部分,小明是value部分。
關鍵問題是,HashMap中key能夠是任意類型,咱們須要一種方式,將任意類型的key,與數組聯繫起來,準確說是與數組的下標聯繫起來,即:任意類型key--->轉換--->數組下標。這裏的轉換,即轉換函數,就是上圖中左邊的散列函數hash(key)。這麼解釋之後,相信左邊散列函數的做用,你能夠理解了。
最後,右邊的鏈表究竟是什麼用意呢?它是解決散列衝突的方法之一拉鍊法,還有另一個解決散列衝突的方法開放尋址法。這裏我先不解釋關於拉鍊法,與開放尋址法的區別。咱們重點關注散列(hash)衝突的問題,爲何就衝突了呢?
咱們知道,要把數據放入HashMap容器中,必然有一個過程,即把任意類型的key,轉換成數組下標的過程。咱們知道容器是已知,且容量是有限的,好比說只能放10個元素的容器;可是要放入容器的元素是未知,無限的。將未知無限的空間,轉換到已知有限的空間,必然會有衝突存在。這段話很抽象,你估計在懷疑這是人話嗎?
我舉個例子,你就明白了:1+4=5,2+3=5,0+5=5,你看等號左邊不一樣,可是右邊結果都是5。也就是說hash(key):hash(1+4),hash(2+3),hash(0+5),不一樣的key,通過hash函數後,會有相同值的狀況出現,這就是hash衝突的由來。你看能夠理解了吧。
那既然hash衝突機率上必定會發生,發生機率的大小,取決於hash函數的設計,好的hash函數設計是一件挺有難度的事情,這是另一個話題,咱們暫時不關心。咱們將重點放在發生衝突之後怎麼處理?這就是我須要給你解釋的上圖中右邊部分的鏈表解決的問題,若是發生了hash衝突,把衝突的元素經過鏈表串起來。
圖解這部份內容,多少會有些抽象,你須要結合圖一塊兒多看一下,一旦看明白了其實也不難。我簡單總結一下關於HashMap:
-
左邊散列函數:用於將任意類型的key,轉換成數組的下標,與數組聯繫起來
-
中間數組:HashMap的底層數據結構,HashMap中的元素其實是存儲在數組中
-
右邊鏈表:由於HashMap中有發生hash衝突,鏈表用於將衝突的多個元素串連起來
2.2.源碼與差別化分析
2.2.1.關鍵源碼分析
這裏我把HashMap的關鍵源碼列舉出來,只能起到拋磚引玉的效果,我建議你實際打開完整的源碼看一看會更好
/* *HashMap底層數據結構:數組 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /* *HashMap默認初始容量:16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /* *HashMap默認負載因子:0.75f */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /* *HashMap每次擴容:都是原容量的2倍 */ // 擴容調用 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // 在原有容量上,擴容2倍 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } // 實際擴容方法 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(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
2.2.2.差別分析:jdk8與jdk7
關於HashMap差別化分析,實現細節上的差別分析,有兩個緯度:
-
針對不一樣的jdk版本(主要是jdk8,與jdk8之前)
-
差別內容:hash衝突解決方式
前面咱們分析了,HashMap中若是發生hash衝突後,經過鏈表(拉鍊法)將衝突的多個元素串聯起來,解決hash衝突。你須要注意,這是jdk7及之前版本的解決方案。
在jdk8中,除了原有的拉鍊法,即鏈表方式解決hash衝突外,還引入了紅黑樹的解決方案。即當某個點衝突元素個數小於8的時候,仍是經過鏈表解決hash衝突;當衝突元素個數大於等於8之後,將鏈表轉換成紅黑樹解決hash衝突。
文字描述始終比較抽象,咱們經過分享兩個圖直觀看一下,方便你理解:
jdk7拉鍊法解決hash衝突:
jdk8拉鍊法、紅黑樹解決hash衝突: