因爲Java 1.7
和Java 1.8
的HashMap
的HashMap
中的put()
和get()
方法在實現上差別很大,因此本文將於分別分析這兩個版本的put()
和get()
f方法源碼分析
下面將會分析這部分的源碼,若是以爲源碼分析內容太囉嗦,能夠跳過源碼部分,直接看源碼下面的總結。學習
HashMap
的put()
方法是咱們最經常使用的方法,可是put()
方法是怎麼工做的呢?this
public V put(K key, V value) { if (key == null)// 處理key爲null的狀況 return putForNullKey(value); // 計算key的hash值 int hash = hash(key); // 計算命中table的索引 int i = indexFor(hash, table.length); // 遍歷命中的鏈表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 存在key和hash值相同則替換value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 記錄結構性變化 modCount++; // 增長新鏈表 addEntry(hash, key, value, i); // 上一次節點不存在,返回null return null; }
put()
方法其實是spa
key
爲null
時,直接調用putForNullKey()
方法。不然進入下一步hash()
方法獲取key
的hash
值,進入下一步indexFor()
計算命中的散列表table
的索引key
和hash
值相同的節點,則建立新的鏈表或尾部添加節點,不然替換對應節點的value
private V putForNullKey(V value) { // 遍歷鏈表,可是命中的散列表的索引和key的hash值爲0 // 後續邏輯與`put()`相似 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
putForNullKey
只是將命中散列表table
的索引和key
的hash
值都設置爲0
,其餘邏輯與put()
方法後續的邏輯一致。指針
/** * 計算命中散列表的索引 */ static int indexFor(int h, int length) { // 等價於length%h return h & (length-1); }
/** * hash值計算方法 */ final int hash(Object k) { int h = 0; // 使用替代的hash方法 if (useAltHashing) { if (k instanceof String) { // 爲字符串則使用特定的hash方法 return sun.misc.Hashing.stringHash32((String) k); } // 使用特定的hash種子計算hash值 h = hashSeed; } h ^= k.hashCode(); // 這部分代碼是爲了減小哈希碰撞 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
void addEntry(int hash, K key, V value, int bucketIndex) { // 判斷散列表是否須要擴容或者未初始化 if ((size >= threshold) && (null != table[bucketIndex])) { // 散列表擴容爲原來的2倍 resize(2 * table.length); // 計算key的hash值,key爲null則返回0 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 建立新的鏈表 // 若是鏈表已存在,則是將新節點插入頭部(頭插法) createEntry(hash, key, value, bucketIndex); }
/** * 頭插法插入新的節點 * 不須要判斷鏈表是否存在 */ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
/** * HashMap的put()方法支持key/value爲null */ public V put(K key, V value) { //其實是先調用HashMap的hash()方法獲取到key的hash值 //而後調用HashMap的putVal()方法 return putVal(hash(key), key, value, false, true); }
put()
方法其實是code
hash()
方法獲取到key
的hash
值putVal()
方法存儲key-value
核心方法是putVal()
方法,下面我會先分析一下hash()
方法,由於這個方法涉及到hash
值這個關鍵屬性的計算。對象
static final int hash(Object key) { int h; // key爲null時,hash值爲0 // key不爲null時,調用key對象的hashCode()方法並經過位運算異或和無符號右移將高位分散到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hash()
方法指定了null
的hash
值爲0
。這樣就能夠支持key
爲null
。(h = key.hashCode()) ^ (h >>> 16)
這段代碼經過位運算異或和無符號右移將高位分散到低位,這樣作能夠減小哈希碰撞的機率(這塊不是很清楚原理,是從方法註釋上了解到的)/** * Map.put()方法的實際實現 * * @param hash key的hash值 * @param key 鍵值對中的key * @param value 鍵值對中的value * @param onlyIfAbsent 若是爲true,則鍵值對中的值已經存在則不修改這個值 * @param evict 若是爲false,則是處於建立模式 * @return 上一次的value,若是上一次的value不存在,則爲null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab用於暫存散列表table。p爲散列表中對應索引的鏈表的頭節點的指針。n存儲tab的長度。i則爲命中的散列表的索引 Node<K,V>[] tab; Node<K,V> p; int n, i; //給tab和n賦值 //當tab爲null或者tab的長度n爲0時,觸發resize()來初始化tab if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //使用(n - 1) & hash(等價於hash%n)計算命中的散列表索引,同時判斷散列表對應索引的鏈表是否存在 if ((p = tab[i = (n - 1) & hash]) == null) //散列表對應索引的鏈表不存在則建立一個新的鏈表 tab[i] = newNode(hash, key, value, null); else {//散列表對應索引的鏈表已存在 Node<K,V> e; K k; // 判斷頭節點的hash值和key是否與入參的hash值和key一致。須要注意,null的hash值爲0 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 對應的鍵值對已經存在,記錄下來 e = p; else if (p instanceof TreeNode)//判斷對應的鏈表是否轉化爲紅黑樹 //如果,則直接調用紅黑樹的putTreeVal()方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//鏈表的頭節點與新的鍵值對不重複,即沒有發生哈希碰撞 for (int binCount = 0; ; ++binCount) {//遍歷鏈表 if ((e = p.next) == null) {//遍歷到尾節點 //尾插法添加一個新的節點 p.next = newNode(hash, key, value, null); //鏈表長度大於閾值 if (binCount >= TREEIFY_THRESHOLD - 1) // 從-1開始,因此爲閾值-1 // 將鏈表轉化爲紅黑樹 treeifyBin(tab, hash); // 中斷循環 break; } // 判斷當前遍歷的節點的hash值和key是否與入參的hash值和key一致,即key是否已經存在 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // key已經存在,中斷循環 break; // 記錄當前遍歷的節點 p = e; } } if (e != null) { // Map中存在重複的key V oldValue = e.value;//記錄下舊值 if (!onlyIfAbsent || oldValue == null)//判斷值存在是否能夠進行修改以及舊值是否爲null e.value = value;//修改該節點的值 afterNodeAccess(e);// 鏈表節點的回調方法,此處爲空方法 return oldValue;//返回舊值 } } // HashMap發生結構變化,變化次數累加 ++modCount; // 鍵值對個數自增,同時判斷是否達到擴容的閾值 if (++size > threshold) resize(); // 鏈表節點的回調方法,此處爲空方法 afterNodeInsertion(evict); // 此處返回null是由於鏈表新增了節點,因此上一次的值必然爲null return null; }
putVal()
方法的關鍵點:索引
table
沒有初始化則調用reszie()
方法初始化。(n - 1) & hash
(等價於hash%n
)。其中n
爲散列表長度,hash
爲插入的鍵值對的key
的哈希值。null
,若爲null
,則建立鏈表,不然進入下一步。key
和hash
一致,若一致則替換該節點的值爲value
,不然進入下一步putTreeVal()
方法遍歷紅黑樹,不然遍歷鏈表。key
和hash
相同的節點就替換對應節點的值value
,若不存在則插入新的樹節點。key
和hash
相同的節點就替換對應節點的值爲value
。若找不到key
和hash
相同的節點,則鏈表尾部插入節點,同時進入下一步。TREEIFY_THRESHOLD(8)
時,則將鏈表轉化爲紅黑樹。除了HashMap
的put()
方法外,get()
方法也是一個咱們經常使用的方法,下面開始分析其關鍵的源碼。rem
public V get(Object key) { if (key == null)// key爲null時特殊處理 return getForNullKey(); // 關鍵獲取key對應value的代碼 Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
get()方法
的關鍵點以下:字符串
key
爲null
,則調用getForNullKey()
方法獲取value
,不然進入下一步getEntry()
方法獲取對應的Entry
對象Entry
對象爲null
時返回null,不然調用getValue()
返回其value
private V getForNullKey() { // 命中散列表索引爲0,無需計算key的hash值 // 遍歷命中的鏈表 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
final Entry<K,V> getEntry(Object key) { // 計算key的hash值,key爲null時返回0 int hash = (key == null) ? 0 : hash(key); // 遍歷命中的鏈表 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } // 鏈表不存在或鏈表中不存在key和hash一致的節點 return null; }
/** * 返回key對應的value,若是不存在則返回null */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get()
方法其實是
hash()
方法獲取到key
的hash
值getNode()
方法經過key
和hash
獲取對應的value
。不存在則返回null
核心方法是getNode()
方法,下面我會先分析一下getNode()
方法。
/** * Map.get()方法的實際實現 * @param hash key的哈希值 * @param key 查詢用的key * @return 節點或者是節點不存在是返回null */ final Node<K,V> getNode(int hash, Object key) { //tab用於暫存散列表table。first爲散列表中對應索引的鏈表的頭節點的指針。n存儲tab的長度。i則爲命中的散列表的索引 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //初始化方法內的變量,同時嘗試命中散列表 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// 老是先檢查鏈表的頭節點 return first;//頭節點符合直接返回頭節點 if ((e = first.next) != null) {//是否只有一個節點 if (first instanceof TreeNode)//判斷頭節點是否爲紅黑樹節點 return ((TreeNode<K,V>)first).getTreeNode(hash, key);//改成遍歷紅黑樹 do {//遍歷鏈表是否有符合的節點 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //不存在對應的key,返回null return null; }
getNode()
方法的關鍵點:
table
不爲null
且長度
大於0
且其索引爲(n - 1) & hash
(等價於hash%n
)的節點不爲null
。其中n
爲散列表長度,hash
爲插入的鍵值對的key
的哈希值。則進入下一步,不然直接返回null
key
和hash
是否與入參一致,若相同則返回首節點,不然進入下一步。1
個,如果則返回null
,不然進入下一步key
和hash
與入參相同的節點,若找到則返回該節點,不然返回null
put()
和get()
方法是HashMap
的經常使用方法,經過學習其源碼瞭解到HashMap
是如何使用拉鍊法解決哈希衝突。而下面將會經過兩幅圖展現put()
和get()
的執行過程:
put()
方法圖解get()
方法圖解put()
方法圖解get()
方法圖解既然分析了Java 1.7
和Java 1.8
中HashMap
的put()
和get()
方法,固然少不了對兩者的比較:
Java 1.7
的HashMap
中存在不少重複的代碼。例如putForNullKey()
和put()
方法中重複的鏈表遍歷,大量重複的hash
值計算邏輯等等。而在Java 1.8
中則對這部分的代碼進行了重構。例如將putForNullKey()
和put()
方法重複的代碼整合成putVal()
方法,hash()
方法處理key
爲null
時的狀況。Java 1.8
中的put()
方法會在鏈表超過樹化閾值的時候,將鏈表轉化爲紅黑樹。而Java 1.7
中則只有鏈表Java 1.7
的鏈表節點插入爲頭插法(不須要判斷鏈表是否存在),而Java 1.8
的鏈表節點插入則爲尾插法。Java 1.8
增長了對putIfAbsent()
方法(存在才進行更新)的支持,詳情能夠看putVal()
中關於onlyIfAbsent
參數的處理邏輯。