學習HashMap的筆記
對於HashMap只是學習了下put,remove方法,hashMap是數組+鏈表+紅黑樹組成
所以下面貼出我自己給代碼的註釋,看不懂的見諒哈,畢竟我也是剛了解,如果有錯誤的地方請指出,非常感謝
put方法(圖片和代碼一起吧,屏幕小的時候 看圖片合適點,看圖片的話建議下載下來看,比較清晰):
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; intView Coden, i; if ((tab = table) == null || (n = tab.length) == 0)//判斷table是否為null或者table的長度是否為0 n = (tab = resize()).length;//調整table的長度 if ((p = tab[i = (n - 1) & hash]) == null)//根據hash去獲取table中的位置,如果為null直接插入 tab[i] = newNode(hash, key, value, null); else {//如果當前數組位置已經存在,表示沖突了 Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//如果原數組中的元素和當前元素(需要新增的)hash一樣並且{key相等或者(key不等於null並且key的equals相等)} e = p;//把當前數組中的元素賦值給e else if (p instanceof TreeNode)//如果當前數組中沖突的節點為紅黑樹 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//插入紅黑樹 else {//這裏表示在沖突的hash桶中去查找為null的位置然後插入(有可能會遇見到達鏈表定義長度,這時需要轉換成紅黑樹了) 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//這裏表示超出定義的鏈表長度了,然後轉換成紅黑樹 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//這裏的條件和上面的判斷一樣,結果是為了如果找到hash想的並且key相等的表示存在 break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null)//如果onlyIfAbsent為true表示不覆蓋原有的值(默認為false) e.value = value;//覆蓋值 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold)//如果當前map的大小大於閾值了,進行擴容 resize(); afterNodeInsertion(evict); return null; }
remove方法:
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {//如果table不等於null並且table的長度大於0 並且p(根據需要刪除的元素的hash值去查詢)是否在table中 Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//如果當前刪除元素的hash值和查找到的元素hash值一樣並且key相等或者(key不等於null並且key的equals相等)表示這個p元素就是需要刪除的元素 node = p;//把p賦值給node else if ((e = p.next) != null) {//否則的話表示都在一個hash桶中,在桶(鏈表)中查找 if (p instanceof TreeNode)//如果是紅黑樹 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//根據key和hash獲取需要刪除的元素 else {//如果不是紅黑樹,則表示是鏈表--進行遍歷 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e;//如果找到則賦值給node break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {//如果node不等於null並且(如果matchValue為false,則不用比較值是否相等)--這裏表示找到了需要刪除的元素 if (node instanceof TreeNode)//如果找到的刪除元素是紅黑樹 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//紅黑樹刪除 else if (node == p)//如果刪除的元素和p等價 tab[index] = node.next;//把table下標的元素值設置成node(需要刪除的元素下一個節點) else p.next = node.next;//把node(需要刪除的節點)下一個節點給node的父節點的下一個節點 ++modCount;//HashMap結構被修改的次數++ --size;//容量-- afterNodeRemoval(node); return node; } } return null; }View Code
get方法相對來說比較簡單,請讀者自己看吧~嘿嘿
關於Hash方法原理(轉載:https://www.zhihu.com/question/20733617)我也是不太明白~:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Java 7中是這樣的
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
上面這段代碼其實叫做"擾動函數"
下面摘自https://www.zhihu.com/question/20733617
大家都知道上面代碼裏的key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。
理論上散列值是一個int型,如果直接拿散列值作為下標訪問HashMap主數組的話,考慮到2進制32位帶符號的int表值範圍從-2147483648到2147483648。前後加起來大概40億的映射空間。只要哈希函數映射得比較均勻松散,一般應用是很難出現碰撞的。
但問題是一個40億長度的數組,內存是放不下的。你想,HashMap擴容之前的數組初始大小才16。所以這個散列值是不能直接拿來用的。用之前還要先做對數組的長度取模運算,得到的余數才能用來訪問數組下標。源碼中模運算是在這個indexFor( )函數裏完成的。
bucketIndex = indexFor(hash, table.length);
indexFor的代碼也很簡單,就是把散列值和數組長度做一個"與"操作,
static int indexFor(int h, int length) {
return h & (length-1);
}
順便說一下,這也正好解釋了為什麽HashMap的數組長度要取2的整次冪。因為這樣(數組長度-1)正好相當於一個“低位掩碼”。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。以初始長度16為例,16-1=15。2進制表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。
10100101 11000100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101 //高位全部歸零,只保留末四位
但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最後幾位的話,碰撞也會很嚴重。更要命的是如果散列本身做得不好,分布上成等差數列的漏洞,恰好使最後幾個低位呈現規律性重復,就無比蛋疼。
這時候“擾動函數”的價值就體現出來了,說到這裏大家應該猜出來了。看下面這個圖,
右位移16位,正好是32bit的一半,自己的高半區和低半區做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。
最後我們來看一下Peter Lawley的一篇專欄文章《An introduction to optimising a hashing strategy》裏的的一個實驗:他隨機選取了352個字符串,在他們散列值完全沒有沖突的前提下,對它們做低位掩碼,取數組下標。
結果顯示,當HashMap數組長度為512的時候,也就是用掩碼取低9位的時候,在沒有擾動函數的情況下,發生了103次碰撞,接近30%。而在使用了擾動函數之後只有92次碰撞。碰撞減少了將近10%。看來擾動函數確實還是有功效的。
但明顯Java 8覺得擾動做一次就夠了,做4次的話,多了可能邊際效用也不大,所謂為了效率考慮就改成一次了。
感覺大概就是為了進行稀釋碰撞(沖突)的次數--純屬個人理解
學習HashMap的筆記