高級併發編程系列十五(一文搞懂HashMap)

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衝突:

相關文章
相關標籤/搜索