這篇文章記錄一下hashmap的學習過程,文章並無涉及hashmap整個源碼,只學習一些重要部分,若有表述錯誤還請在評論區指出~html
Hashmap採用key算hash映射到具體的value,所以查找效率爲o(1),爲防止hash衝突,在數組的基礎上加入鏈表、紅黑樹,爲無序非線程安全的存儲結構java
jdk1.8以前採用如下方式存儲數據:node
左邊實際上就是一個數組,右邊則是key值相同的元素放到同一個鏈表中(圖片侵刪)數組
可是這種數組加單鏈表也存在問題,即單鏈表長度過長時,搜索值將耗費時間複雜度爲o(n),所以jdk1.8中提出數組+鏈表+紅黑樹的方法安全
該類是實現map接口的,而且也支持序列化、支持淺拷貝多線程
第一種能夠本身指定容量大小與負載因子,那麼此時閾值已經肯定,使用tableSizefor來找到大於等於指定容量的最小2的次方數做爲閾值,其中輸入的值先-1,保證返回的值要大於等於輸入值app
第二種能夠僅指定容量,使用默認的負載因子,此時也會初始化閾值post
第三種使用默認的容量16以及默認的負載因子0.75性能
第四種是由map來建立一個hashmap,使用默認的負載因子,以及可以將map放進hashmap的容量建立(不經常使用)學習
默認容量1左移4位位16,這裏容量大小必須爲2的次方,頗有講究 ,後面解釋緣由
最大容量爲2的30次方
默認的負載因子0.75,和擴容相關,主要表示當前hashmap的填充度
node表,真正存儲元素的表,爲2的次方,其爲hashmap的一個內部類
static class Node<K,V> implements Map.Entry<K,V> { final int hash; //key的hash值 final K key; V value; Node<K,V> next; //存儲下一個節點的地址 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
<key,value>元素的個數,包括數組中的和鏈表中的元素
put方法,放入鍵值對:
首先將放入的鍵計算hash,而後調用putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i;
//若是當前hash表爲空,即尚未放入任何元素,則進行擴容操做,至關於初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//根據當前key的hash算出當前元素應該放到hash表中的下標,若是改位置爲null,則放入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //不然發生hash衝突,而且若是當前位置元素的hash和要放入元素的hash相同而且當前元素的key和要放入的key同樣,則暫時保存當前衝突的node節點 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
//若僅僅鍵的hash同樣,可是key並不同則首先判斷是不是紅黑樹節點,若是是的話則將當前的鍵放進紅黑樹中,更新當前的hash表的衝突節點 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //不然當前節點爲鏈表
else {
//遍歷鏈表(由於咱們以前已經知道每一個node節點都存儲了下一個節點的地址,因此P.next變量即表明相對於當前node的下一個node,那麼遍歷到一個鏈表的尾部放入新的節點便可) for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //放入後判斷,若是當前hash表的長度>=7,則將當前hash位置處轉爲紅黑樹表示從而替換鏈表表示 treeifyBin(tab, hash); break; }
//若是遍歷過程當中發現鏈表中存在相同的key則break退出 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; //不然更新p節點爲e,從而實現循環遍歷鏈表 } }
//若是保存衝突節點的e變量不爲null,則取衝突的值,根據onlyIfAbsent沒有設置或者當前value爲null,都將 if (e != null) { // existing mapping for key V oldValue = e.value; //取到衝突節點的value if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //hashmap修改次數,防止多線程衝突的 if (++size > threshold) //判斷當前node節點的多少有沒有到擴容的閾值 resize(); afterNodeInsertion(evict); return null; }
因此整個put的流程爲:
①.首先根據要放入的key計算hash,而後根據hash獲取table中的放入位置,若是當前table爲空,則進行初始化
②.判斷放入位置是否爲空,爲空則直接放入,不然判斷是否爲紅黑樹節點,不是則爲鏈表,則遍歷鏈表查找是否存在相同的key,沒找到則放入鏈表尾部並判斷是否須要轉爲紅黑樹(TREEIFY_THRESHOLD)
③.若查找鏈表找到相同key則替換,放入後要判斷node節點數是否超過threshold,判斷是否須要resize
resize方法,擴充當前容量:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //保存舊的hash表 int oldCap = (oldTab == null) ? 0 : oldTab.length; //判斷hash表的長度,如果第一次初始化則爲0 int oldThr = threshold; //取舊的閾值 int newCap, newThr = 0; //定義新的長度和閾值 if (oldCap > 0) { //若是以前長度大於零 if (oldCap >= MAXIMUM_CAPACITY) { //若是以前的長度大於等於2的30次 threshold = Integer.MAX_VALUE; //則將node節點閾值設置爲2的31次-1 return oldTab; //返回舊的hash表,再也不擴容 }
//不然知足擴容條件,進行擴容 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //若是舊的容量擴大一倍小於2的30次而且舊的容量大於默認的初始化容量大小16,閾值也變爲原來的2倍 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 則容量擴大一倍 }
//若是舊的容量爲0,可是舊的閾值大於零,則多是初始化hashmap時指定了容量,則直接將新的容量設置爲舊的閾值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;
//對於沒有設置初始容量的狀況 else { // zero initial threshold signifies using defaults //若是是第一次初始化,則設置容量爲16,閾值爲16*0.75=12,即hashmap能夠放12個node節點 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
//若是新的閾值爲0,則進行修正,令新的閾值爲新的hash表容量長*負載因子 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }
//設置完新的容量和新的閾值後,則開始進項node節點元素轉移 threshold = newThr; //先將新生成的閾值賦值給成員變量threshold @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //而後聲明一個新的節點數組,容量即爲擴充後的大小 table = newTab; //替換成員標量table爲新表 if (oldTab != null) { //遍歷舊的容量大小,取其每一個node節點 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //若是該節點不爲null oldTab[j] = null; //則讓舊錶的該位置爲null,進行垃圾回收 if (e.next == null) //若是當前遍歷的節點下一個爲null,說明爲尾節點(單個node節點,無鏈表,無紅黑樹) newTab[e.hash & (newCap - 1)] = e; //則直接將該節點放到新的hash表中 //若是下一個節點不爲null,則判斷當前節點是不是紅黑樹節點,如果,則將新標的該節點轉爲紅黑樹節點
else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //不然爲單鏈表節點,則遍歷當前鏈中的節點決定要放入新hash表的位置
else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next;
do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
這裏設計很妙,原來的容量爲2的次方,則只有1位爲1,原來的下標是容量-1,則新增的一位bit,決定了節點hash新增的一位爲1仍是爲0,來決定其存放位置,其也爲隨機的,從而均勻地將節點放到新的hash表中,新增一位爲0則放到低位中,即索引值不變,新增一位爲1,則放到高位中,這樣本來在一條鏈中的節點就可以分佈到兩條鏈上,也減小了搜索的開銷
1.jdk1.7中發生hash衝突新節點採用頭插法,1.8採用的爲尾插法
2.1.7採用數組+鏈表,1.8採用的是數組+鏈表+紅黑樹
3.1.7在插入數據以前擴容,而1.8插入數據成功以後擴容
1.在算key的hash時將key的hashcode和與hashcode的高16位作異或下降hash衝突機率
2.HashMap 的 bucket (數組)大小必定是2的n次方,便於後面等效取模以及resize時定節點分佈(low或者high)
3.HashMap 在 put 的元素數量大於 Capacity * LoadFactor(默認16 * 0.75)=12 以後會進行擴容,負載因子大於0.75則會減少空間開銷,
4.影響hashmap性能的兩個參數就是負載因子和初始容量,擴容影響性能,所以最好能提早根據負載所以估算hashmap大小,擴容其實是將當前node節點放入一個新的node數組
5.tab[i = (n - 1) & hash] 實際上用與運算代替取模操做,性能更好,n即爲容量大小,n爲2的次方,則n-1則其二進制位爲全1,從而代替模運算,e.hash & oldCap 用與運算決定hash增長的一位爲0或者爲1
關於負載因子設置:
負載因子的大小決定了HashMap的數據密度。
負載因子越大密度越大,發生碰撞的概率越高,數組中的鏈表越容易長,形成查詢或插入時的比較次數增多,性能會降低。
負載因子越小,就越容易觸發擴容,數據密度也越小,意味着發生碰撞的概率越小,數組中的鏈表也就越短,查詢和插入時比較的次數也越小,性能會更高。可是會浪費必定的內容空間。並且常常擴容也會影響性能,建議初始化預設大一點的空間。
按照其餘語言的參考及研究經驗,會考慮將負載因子設置爲0.7~0.75,此時平均檢索長度接近於常數
https://zhuanlan.zhihu.com/p/72296421
http://www.javashuo.com/article/p-ftqonsun-kq.html
http://www.javashuo.com/article/p-abqiemse-gx.html
https://tech.meituan.com/2016/06/24/java-hashmap.html
http://www.javashuo.com/article/p-niykhntj-n.html
http://www.javashuo.com/article/p-eiwmdbul-cu.html 1.7和1.8區別