HashMap在JDK7和JDK8中的區別

[深刻淺出集合Map]中,已講述了HashMap在jdk7中實現,在此就再也不細說了java

JDK7中的HashMap

基於鏈表+數組實現,底層維護一個Entry數組數組

Entry<K,V>[] table;

根據計算的hashCode將對應的KV鍵值對存儲到該table中,一旦發生hashCode衝突,那麼就會將該KV鍵值對放到對應的已有元素的後面, 此時,造成了一個鏈表式的存儲結構,以下圖安全

image

JDK8中的HashMap

基於位桶+鏈表/紅黑樹的方式實現,底層維護一個Node數組數據結構

Node<K,V>[] table;

在JDK7中HashMap,當成百上千個節點在hash時發生碰撞,存儲一個鏈表中,那麼若是要查找其中一個節點,那就不可避免的花費O(N)的查找時間,這將是多麼大的性能損失,這個問題終於在JDK8中獲得瞭解決。多線程

JDK8中,HashMap採用的是位桶+鏈表/紅黑樹的方式,當鏈表的存儲的數據個數大於等於8的時候,再也不採用鏈表存儲,而採用了紅黑樹存儲結構。這是JDK7與JDK8中HashMap實現的最大區別。
以下圖所示:併發

image

這麼作主要是再查詢的時間複雜度上進行優化,鏈表爲O(n),而紅黑樹一直是O(logn),衝突(即爲相同的hash值存儲的元素個數) 超過8個,能夠大大的提升查找性能。oop

其餘異同

共同點

1.容量(capacity):容量爲底層數組的長度,JDK7中爲Entry<k,v style="margin: 0px; padding: 0px; max-width: 100%; overflow-wrap: break-word !important; box-sizing: border-box !important; font-size: inherit; color: inherit; line-height: inherit;">數組,JDK8中爲Node<k,v style="margin: 0px; padding: 0px; max-width: 100%; overflow-wrap: break-word !important; box-sizing: border-box !important; font-size: inherit; color: inherit; line-height: inherit;">數組
a. 容量必定爲2的次冪</k,v></k,v>性能

static int indexFor(int h, int length) {      return h & (length-1);  }  

這段代碼是用來計算出鍵值對存放在一個數組的索引,h是int hash = hash(key.hashCode())計算出來的,SUN大師們發現, 「當容量必定是2^n時,h & (length - 1) == h % length」 ,按位運算特別快 。
源碼中大量使用運算,對於計算機,位運算計算效率特別快,畢竟二進制纔是親兒子呀學習

b. 默認初始容量16(容量爲低層數組的長度,JDK7中爲Entry<k,v style="margin: 0px; padding: 0px; max-width: 100%; overflow-wrap: break-word !important; box-sizing: border-box !important; font-size: inherit; color: inherit; line-height: inherit;">數組,JDK8中爲Node<k,v style="margin: 0px; padding: 0px; max-width: 100%; overflow-wrap: break-word !important; box-sizing: border-box !important; font-size: inherit; color: inherit; line-height: inherit;">數組)</k,v></k,v>優化

c.最大容量1<<30,即2的30次方

1 << 30 = 10737418241 << 31 = -21474836481 << 32 = 11 << 33 = 21 << -1 = -2147483648

hashmap的「最大容量「實際上是Integer.MAX_VALUE

2.加載因子(Load factor):HashMap在其容量自動增長前可達到多滿的一種尺度
a.  默認加載因子 = 0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f 
  • 加載因子越大、填滿的元素越多 = 空間利用率高、但衝突的機會加大、查找效率變低(由於鏈表變長了)
  • 加載因子越小、填滿的元素越少 = 空間利用率小、衝突的機會減少、查找效率高(鏈表不長)
    0.75是一個"衝突的機會"與"空間利用率"之間尋找一種平衡與折衷的選擇

3.擴容機制:擴容時resize(2 * table.length),擴容到原數組長度的2倍。

4.key爲null:若key == null,則hash(key) = 0,則將該鍵-值 存放到數組table 中的第1個位置,即table [0]

   static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }
不一樣點

1.發生hash衝突時 JDK7:發生hash衝突時,新元素插入到鏈表頭中,即新元素老是添加到數組中,就元素移動到鏈表中。
JDK8:發生hash衝突後,會優先判斷該節點的數據結構式是紅黑樹仍是鏈表,若是是紅黑樹,則在紅黑樹中插入數據;若是是鏈表,則將數據插入到鏈表的尾部並判斷鏈表長度是否大於8,若是大於8要轉成紅黑樹。

2.擴容時 JDK7:在擴容resize()過程當中,採用單鏈表的頭插入方式,在將舊數組上的數據 轉移到 新數組上時,轉移操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉移數據、擴容後,容易出現鏈表逆序的狀況 。
多線程下resize()容易出現死循環。此時若(多線程)併發執行 put()操做,一旦出現擴容狀況,則 容易出現 環形鏈表,從而在獲取數據、遍歷鏈表時 造成死循環(Infinite Loop),即 死鎖的狀態 。

JDK8:因爲 JDK 1.8 轉移數據操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的尾部依次插入,因此不會出現鏈表 逆序、倒置的狀況,故不容易出現環形鏈表的狀況 ,但jdk1.8還是線程不安全的,由於沒有加同步鎖保護。

建議: 1.使用時設置初始值,避免屢次擴容的性能消耗
2.使用自定義對象做爲key時,須要重寫hashCode和equals方法
3.多線程下,使用CurrentHashMap代替HashMap

推薦閱讀:

「深刻淺出」集合Map

javaWeb傳收參數方式總結

週末了,笑久一點~

若是以爲不錯,請給個「好看」

分享給你的朋友!

image

image

 THANDKS

  • End -

一個立志成大腿而天天努力奮鬥的年輕人

伴學習伴成長,成長之路你並不孤單!

 掃描二維碼,關注公衆號

相關文章
相關標籤/搜索