HashMap實現了Map接口,繼承AbstractMap。其中Map接口定義了鍵映射到值的規則,而AbstractMap類提供 Map 接口的骨幹實現,以最大限度地減小實現此接口所需的工做node
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable面試
HashMap基於哈希表的 Map 接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了不一樣步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。數組
值得注意的是HashMap不是線程安全的,若是想要線程安全的HashMap,能夠經過Collections類的靜態方法synchronizedMap得到線程安全的HashMap。安全
Map map = Collections.synchronizedMap(new HashMap());數據結構
HashMap提供了三個構造函數:app
HashMap():構造一個具備默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。ide
HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。函數
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。源碼分析
在這裏提到了兩個參數:初始容量,加載因子。這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中桶的數量,初始容量是建立哈希表時的容量,加載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。系統默認負載因子爲0.75,通常狀況下咱們是無需修改的。性能
HashMap的底層主要是基於數組和鏈表來實現的,它之因此有至關快的查詢速度主要是由於它是經過計算散列碼來決定存儲的位置。HashMap中主要是經過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就同樣。若是存儲的對象對多了,就有可能不一樣的對象所算出來的hash值是相同的,這就出現了所謂的hash衝突。學過數據結構的同窗都知道,解決hash衝突的方法有不少,HashMap底層是經過鏈表來解決hash衝突的。
HashMap是基於哈希表的 Map 接口的實現。既然是基於哈希表的那麼他的數據結構天然也就是由數組和鏈表組成的嘍。數組的特色是:尋址容易,插入和刪除困難;而鏈表的特色是:尋址困難,插入和刪除容易。因此兩者結合就是哈希表了。下面借用一張圖片看看它的內部構造。
從圖上能夠看出左側是一個長度爲16的數組,右側則是存儲數據的鏈表。那數組的下標是按什麼順序存儲的呢?其實存儲規則是這樣的,當拿到Map的key之後須要調用hashcode()方法計算他的hash值,而後用hash%len(數組長度)獲得的結果就是它對應的順序了。看看上面的十二、2八、10八、140對16作模運算都等於12 因此他們都在12上面。那咱們的key value去哪裏了呢?HashMap裏面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。
結構:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
HashMap其實就是一個Entry數組,Entry對象中包含了鍵和值,其中next也是一個Entry對象,它就是用來處理hash衝突的,造成一個鏈表。
有兩點須要注意:
一是鏈的產生。這是一個很是優雅的設計。系統老是將新的Entry對象添加到bucketIndex處。若是bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,造成一條Entry鏈,可是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。
2、擴容問題。
隨着HashMap中元素的數量愈來愈多,發生碰撞的機率就愈來愈大,所產生的鏈表長度就會愈來愈長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必需要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。可是擴容是一個很是耗時的過程,由於它須要從新計算這些數據在新table數組中的位置並進行復制處理。因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。
1.關鍵屬性
先看看HashMap類中的一些關鍵屬性:
1 transient Entry[] table;//存儲元素的實體數組 2
3 transient int size;//存放元素的個數 4
5 int threshold; //臨界值 當實際大小超過臨界值時,會進行擴容threshold = 加載因子*容量 6 7 final float loadFactor; //加載因子 8
9 transient int modCount;//被修改的次數
其中loadFactor加載因子是表示Hsah表中元素的填滿的程度.
若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:衝突的機會加大了.鏈表長度會愈來愈長,查找效率下降。
反之,加載因子越小,填滿的元素越少,好處是:衝突的機會減少了,但:空間浪費多了.表中的數據將過於稀疏(不少空間還沒用,就開始擴容了)
衝突的機會越大,則查找的成本越高.
所以,必須在 "衝突的機會"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質上是數據結構中有名的"時-空"矛盾的平衡與折衷.
若是機器內存足夠,而且想要提升查詢速度的話能夠將加載因子設置小一點;相反若是機器內存緊張,而且對查詢速度沒有什麼要求的話能夠將加載因子設置大一點。不過通常咱們都不用去設置它,讓它取默認值0.75就行了。
二、構造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
咱們能夠看到在構造HashMap的時候若是咱們指定了加載因子和初始容量的話就調用第一個構造方法,不然的話就是用默認的。默認初始容量爲16,默認加載因子爲0.75。咱們能夠看到上面代碼中13-15行,這段代碼的做用是確保容量爲2的n次冪,使capacity爲大於initialCapacity的最小的2的n次冪,至於爲何要把容量設置爲2的n次冪,咱們等下再看。
3.數據存儲
put 過程圖解
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
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);
return null;
}
第2和3行的做用就是處理key值爲null的狀況,咱們看看putForNullKey(value)方法:
private V putForNullKey(V value) {
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;
}
注意:若是key爲null的話,hash值爲0,對象存儲在數組中索引爲0的位置。即table[0]
計算hash碼的函數 這個咱們要重點說下,咱們通常對哈希表的散列很天然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap中則經過h&(length-1)的方法來代替取模,一樣實現了均勻的散列,但效率要高不少,這也是HashMap對Hashtable的一個改進。
接下來,咱們分析下爲何哈希表的容量必定要是2的整數次冪。首先,length爲2的整數次冪的話,h&(length-1)就至關於對length取模,這樣便保證了散列的均勻,同時也提高了效率;其次,length爲2的整數次冪的話,爲偶數,這樣length-1爲奇數,奇數的最後一位是1,這樣便保證了h&(length-1)的最後一位可能爲0,也可能爲1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣即可以保證散列的均勻性,而若是length爲奇數的話,很明顯length-1爲偶數,它的最後一位是0,這樣h&(length-1)的最後一位確定爲0,即只能爲偶數,這樣任何hash值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,所以,length取2的整數次冪,是爲了使不一樣hash值發生碰撞的機率較小,這樣就能使元素在哈希表中均勻地散列。
這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:
假設數組長度分別爲15和16,優化後的hash碼分別爲8和9,那麼&運算後的結果以下:
從上面的例子中能夠看出:當它們和15-1(1110)「與」的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲15的時候,hash值會與15-1(1110)進行「與」,那麼 最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!而當數組長度爲16時,即爲2的n次方時,2n-1獲得的二進制數的每一個位上的值都爲1,這使得在低位上&時,獲得的和原hash的低位相同,加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值纔會被放到數組中的同一個位置上造成鏈表。
因此說,當數組長度爲2的n次冪的時候,不一樣的key算得得index相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
根據上面 put 方法的源代碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
四、調整大小
resize()方法以下:
從新調整HashMap的大小,newCapacity是調整後的單位
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
新建了一個HashMap的底層數組,上面代碼中第10行爲調用transfer方法,將HashMap的所有元素添加到新的HashMap中,並從新計算元素在新的數組中的索引位置
當HashMap中的元素愈來愈多的時候,hash衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操做也會出如今ArrayList中,這是一個經常使用的操做,而在HashMap數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是resize。
那麼HashMap何時進行擴容呢?當HashMap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲16,那麼當HashMap中元素個數超過160.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,擴容是須要進行數組複製的,複製數組是很是消耗性能的操做,因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。
五、數據讀取
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
六、HashMap的性能參數:
HashMap 包含以下幾個構造器:
HashMap():構建一個初始容量爲 16,負載因子爲 0.75 的 HashMap。
HashMap(int initialCapacity):構建一個初始容量爲 initialCapacity,負載因子爲 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。
HashMap的基礎構造器HashMap(int initialCapacity, float loadFactor)帶有兩個參數,它們是初始容量initialCapacity和加載因子loadFactor。
initialCapacity:HashMap的最大容量,即爲底層數組的長度。
loadFactor:負載因子loadFactor定義爲:散列表的實際元素數目(n)/ 散列表的容量(m)。
負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。
HashMap的實現中,經過threshold字段來判斷HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下容許的最大元素數目,超過這個數目就從新resize,以下降實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是容量的兩倍.
從新調整HashMap大小存在什麼問題?
當從新調整HashMap大小的時候,確實存在條件競爭,由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。
爲何String, Interger這樣的wrapper類適合做爲鍵? String, Interger這樣的wrapper類做爲HashMap的鍵是再適合不過了,並且String最爲經常使用。由於String是不可變的,也是final的,並且已經重寫了equals()和hashCode()方法了。其餘的wrapper類也有這個特色。不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其餘的優勢如線程安全。若是你能夠僅僅經過將某個field聲明成final就能保證hashCode是不變的,那麼請這麼作吧。由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的。若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這樣就能提升HashMap的性能
咱們可使用自定義的對象做爲鍵嗎? 這是前一個問題的延伸。固然你可能使用任何對象做爲鍵,只要它遵照了equals()和hashCode()方法的定義規則,而且當對象插入到Map中以後將不會再改變了。若是這個自定義對象時不可變的,那麼它已經知足了做爲鍵的條件,由於當它建立以後就已經不能改變了。
咱們可使用CocurrentHashMap來代替Hashtable嗎?這是另一個很熱門的面試題,由於ConcurrentHashMap愈來愈多人用了。咱們知道Hashtable是synchronized的,可是ConcurrentHashMap同步性能更好,由於它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap固然能夠代替HashTable,可是HashTable提供更強的線程安全性
下面一張圖能夠看得很清楚。
JDK1.6中HashMap採用的是位桶+鏈表的方式,即咱們常說的散列鏈表的方式,而JDK1.8中採用的是位桶+鏈表/紅黑樹的方式,也是非線程安全的。當某個位桶的鏈表的長度達到某個閥值的時候,這個鏈表就將轉換成紅黑樹。
//鏈表節點
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//省略
}
//紅黑樹節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//省略
}
// HashMap的主要屬性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 槽數組,Node<K,V>類型,TreeNode extends LinkedHashMap.Entry<K,V>,因此能夠存放TreeNode來實現Tree bins
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
// 去掉了volatile的修飾符
transient int modCount;
int threshold;
final float loadFactor;
...
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
//hash & length-1 定位數組下標
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null)
{
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
/*第一個節點是TreeNode,則採用位桶+紅黑樹結構,
* 調用TreeNode.getTreeNode(hash,key),
*遍歷紅黑樹,獲得節點的value
*/
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);
}
}
return null;
}
final TreeNode<K,V> getTreeNode(int h, Object k) {
//找到紅黑樹的根節點並遍歷紅黑樹
return ((parent != null) ? root() : this).find(h, k, null);
}
/*
*經過hash值的比較,遞歸的去遍歷紅黑樹,這裏要提的是compareableClassFor(Class k)這個函數的做用,在某些時候
*若是紅黑樹節點的元素are of the same "class C implements Comparable<C>" type
*利用他們的compareTo()方法來比較大小,這裏須要經過反射機制來check他們究竟是不是屬於同一個類,是否是具備可比較性.
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
//put(K key,V value)函數 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是table爲空或者長度爲0,則resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //找到key值對應的槽而且是第一個,直接加入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //第一個node的hash值即爲要加入元素的hash if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){ e = p; }else if (p instanceof TreeNode)//第一個節點是TreeNode,即tree-bin /*Tree version of putVal. *final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) */ e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //不是TreeNode,即爲鏈表,遍歷鏈表 for (int binCount = 0; ; ++binCount) { /*到達鏈表的尾端也沒有找到key值相同的節點, *則生成一個新的Node,而且判斷鏈表的節點個數是否是到達轉換成紅黑樹的上界 *達到,則轉換成紅黑樹 */ 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)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //返回舊的value值 return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }