HashMap
的總結,有錯誤請留言.HashMap
是在JDK1.2中引入的一種K/V對
形式的集合類.HashMap
經過數組和單鏈表組合的結構形式來存儲數據,數組在這做爲一個外部結構,數組中的每一個節點被稱作Bucket(桶)
,而桶是由在單鏈表構成,JDK1.8
以後爲了解決長鏈表下,查詢和插入效率低下的狀況,又引入了紅黑樹的做爲桶的實現方式,HashMap
定義的Node
內部類生成的,是普通的鏈表節點類.HashMap
是線程不安全的,在JDK1.8
以前多線程狀況下甚至可能會出現環路(後面會講),因此多線程狀態下仍是要使用ConcurrentHashMap
的.HashMap
的參數很少,除去當作默認屬性的靜態常量和底層數組對象,就只有如下五個transient Node<K,V>[] table;
transient int size
transient int modCount;
int threshold;
final float loadFactor;
複製代碼
table
就是整個HashMap
的底層數組,table
的初始化並不在構造函數中完成,而是在resize()
方法中完成.html
table
的初始化可能有點繞,構造函數中最多指定了閾值threshold
和負載因子loadFactor
並無容量相關,可是在resize()
方法中會根據舊容量和舊閾值判斷新容量是等於默認容量,舊閾值或者兩倍舊容量,最後根據新容量建立新數組loadFactor
就是所謂的負載因子,默認爲0.75,是控制擴容時機的關鍵屬性,由於擴容發生在當前元素個數超過閾值時,而閾值等於當前容量乘以負載因子.java
modCount
爲修改計數,是fast-fail
機制的關鍵參數.在對Map
中的元素作新增/刪除操做時會自增,但修改不會(putVal()方法中覆蓋原值)git
HashMap
的新增過程重點主要仍是定位,如何肯定元素在數組中的位置,HashMap
採用的就是Hash算法
HashMap
會根據Key
的hash值,按照表達式(n - 1) & hash
計算出桶的下標Node
,做爲鏈表的第一個元素,直接存放在數組中.(之前還據說過什麼鏈表首節點爲空的狀況,是假的.)TreeNode
的方法插入到樹中.table
是否初始化,新增後會判斷該桶大小是否超過的8,超過則轉化爲紅黑樹,再判斷整個數組是否須要擴容.Hash
同時也叫散列,能夠把任意長度的輸入經過算法,換算成固定長度的輸出,不一樣元素經過Hash
算法得到的下標一致能夠被稱之爲衝突或者碰撞
,Hash
算法的要求就是使元素儘可能少的發生碰撞,從而均勻的散佈在數組中
.而發生碰撞時,像HashMap
這種以一個列表下掛的方式能夠被稱爲拉鍊法
.get()
方法,經過key
值查找的狀況,若是本身遍歷的另說.
(n - 1) & hash
計算出桶的下標(能夠說是至關重要了),若獲得的桶爲空,直接返回nullkey.equals(k)
判斷是否相等JDK1.8
以後引入的紅黑樹做爲桶的另外一種實現方法.當鏈表長度大於8
時,桶的實現會轉化爲紅黑樹
.HashMap
的性能很大一部分取決於Hash
算法..經過插入和查找咱們能夠知道,在數組大小不變的狀況下,鏈表越長或者說樹的高度越高都會致使操做性能下降,因此此時頗有必要經過擴容數組的方式,從新排列桶中元素,下降鏈表長度,減小樹的高度.github
首先,觸發擴容的狀況是size > threshold
即元素個數大於閾值.整個擴容過程能夠簡單的拆分爲如下幾步:算法
1 << 30
也就是2^30.resize()
方法中從新散佈元素的方法仍是頗有意思的(除去單元素鏈表和紅黑樹(桶的容量在1~7之間)shell
lo
和hi
(源碼是loHead和hiHead,我猜是low和high,怎麼縮寫這麼隨意),lo
表示0到舊容量大小部分,hi
表示餘下算是新加入的部分,並以此建立兩個鏈表的節點e.hash & oldCap
判斷元素是否分佈在lo
部分,是就掛到lo
鏈表下面,否就掛到hi
鏈表下面.lo
鏈表掛到和舊數組相同位置的桶,而hi
則掛到下標爲原下標 + 舊數組容量
的桶.e.hash & (oldCap - 1) + oldCap == e.hash & (oldCap << 1) -1
能夠看出resize()
方法會調整所有的元素散列狀況,所以過於頻繁的resize
會下降HashMap
的性能,所以若是一開始能夠大概知道所須要存放的元素個數時,儘可能直接指定容量大小.數組
JDK1.7
以前的resize()
方法在併發條件下可能會發生閉環問題,但在JDK1.8
以後不會在出現,但並不表明HashMap
能夠在併發條件下使用了,小部分狀況仍是會出現數據丟失等問題.安全
介紹JDK1.8
以前的閉環問題詳情的文章多線程
HashMap的懶加載問題併發
HashMap
的源碼,你會發現底層數組table
的建立其實並非在構造函數中完成的,而是resize()
方法中,這就是所謂的懶加載
,數組對象並不是是在一開始就建立的,而是在第一次插入操做以前完成的。static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
擾動函數的邏輯很簡單就是將hashCode
的高16位和低16位異或.
擾動函數的做用就是增長散列的隨機性,使元素可以更均勻的分佈在數組中,減小衝突從而捎帶提升性能.
至於爲何,能夠看hash(*)
用到的地方,hash(*)
被用來計算元素的下標.而下標的計算公式以下
tab[i = (n - 1) & hash] // n表示數組的長度
複製代碼
由於HashMap
的容量必定會是2的次冪,因此減1以後轉化爲二進制會變爲一串0加一串1的,例如長度爲4時,減去1,就會變爲000…00011
(前面30個0),再結合&
能夠發現他只使用了hashCode
的末尾幾位,高位是所有沒用.
而通過擾動函數,將高16位和低16位異或以後至關於高低位都用到了,其散列的隨機性也就增長了.
(length - 1) & hash)
代替hash % length
,相對來講位運算性能更佳,速度更快。(length - 1) & hash
的方式計算下標以後,若是不是二次冪的容量,出現碰撞的概率將會大大增長,例如咱們取17做爲容量((17 -1) => 0001000
),通過&
與運算,能夠想象會有一大批的元素直接掛在0號桶。hash & length
的話,也不用要求容量必定是二次冪,但各方面的性能老是會差一點的。HashTable
都沒用過了,但之前還稍微看過HashTable
是線程安全的,暴力的加方法級synchronized
.而HashMap
是線程不安全的,併發狀況下可能會出現數據丟失等狀況.HashTable
不容許null值,而HashMap
容許null值.(包括key和value)HashCode
的使用不一樣,HashTable
是直接調用hashCode
,而HashMap
會通過擾動函數.並且HashMap
中用&
代替了%
HashTable
數組默認是11,且增加爲2n+1
,而HashMap
默認爲16,增加爲2n
,且硬性要求長度爲2的次冪.HashTable
並非和HashMap
同樣繼承自AbstractMap
的,它繼承自一個獨立的父類AbstractDictionary