hashMap 數據結構
如上圖所示,JDK7之前hashmap又叫散列鏈表:基於一個數組以及多個鏈表的實現,hash值沖突的時候,就將對應節點以鏈表的形式存儲。
JDK8中,當同一個hash值(Table上元素)的鏈表節點數不小於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹。這就是JDK7與JDK8中HashMap實現的最大區別。
其下基於 JDK1.7.0_80 與 JDK1.8.0_66 做的分析
JDK1.7中
使用一個Entry數組來存儲數據,用key的hashcode取模來決定key會被放到數組里的位置,如果hashcode相同,或者hashcode取模后的結果相同(hash collision),那么這些key會被定位到Entry數組的同一個格子里,這些key會形成一個鏈表。
在hashcode特別差的情況下,比方說所有key的hashcode都相同,這個鏈表可能會很長,那么put/get操作都可能需要遍歷這個鏈表
也就是說時間復雜度在最差情況下會退化到O(n)
JDK1.8中
使用一個Node數組來存儲數據,但這個Node可能是鏈表結構,也可能是紅黑樹結構
如果插入的key的hashcode相同,那么這些key也會被定位到Node數組的同一個格子里。
如果同一個格子里的key不超過8個,使用鏈表結構存儲。
如果超過了8個,那么會調用treeifyBin函數,將鏈表轉換為紅黑樹。
那么即使hashcode完全相同,由於紅黑樹的特點,查找某個特定元素,也只需要O(log n)的開銷
也就是說put/get的操作的時間復雜度最差只有O(log n)
聽起來挺不錯,但是真正想要利用JDK1.8的好處,有一個限制:
key的對象,必須正確的實現了Compare接口
如果沒有實現Compare接口,或者實現得不正確(比方說所有Compare方法都返回0)
那JDK1.8的HashMap其實還是慢於JDK1.7的
簡單的測試數據如下:
向HashMap中put/get 1w條hashcode相同的對象
JDK1.7: put 0.26s,get 0.55s
JDK1.8(未實現Compare接口):put 0.92s,get 2.1s
但是如果正確的實現了Compare接口,那么JDK1.8中的HashMap的性能有巨大提升,這次put/get 100W條hashcode相同的對象
JDK1.8(正確實現Compare接口,):put/get大概開銷都在320ms左右
為什么要這么操作呢?
我認為應該是為了避免Hash Collision DoS攻擊
Java中String的hashcode函數的強度很弱,有心人可以很容易的構造出大量hashcode相同的String對象。
如果向服務器一次提交數萬個hashcode相同的字符串參數,那么可以很容易的卡死JDK1.7版本的服務器。
但是String正確的實現了Compare接口,因此在JDK1.8版本的服務器上,Hash Collision DoS不會造成不可承受的開銷。
參考資料:
jdk1.7.0_80的HashMap源碼
jdk1.8.0_66的HashMap源碼