HashMap底層由數組+鏈表+紅黑樹組成,可接受null值,非線程安全node
transient Node<K,V>[] table; //hashmap數組
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認容量16 static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量 static final float DEFAULT_LOAD_FACTOR = 0.75f;//默認負載因子 static final int TREEIFY_THRESHOLD = 8; //鏈表節點轉換紅黑樹節點的閾值 static final int UNTREEIFY_THRESHOLD = 6; //紅黑樹節點轉換鏈表節點的閾值 static final int MIN_TREEIFY_CAPACITY = 64;// 轉紅黑樹時, table的最小長度 // 基本hash節點, 繼承自Entry static class Node<K,V> implements Map.Entry<K,V> { final int 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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } //構造函數 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); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //構造函數 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //構造函數 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } // 紅黑樹節點 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); } /** * Returns root of tree containing this node. */ final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } //... }
HashMap定位數組索引位置,直接決定了hash方法的離散性能。下面是定位哈希桶數組的源碼:算法
static final int hash(Object key) { // 計算key的hash值 int h; // 1.先拿到key的hashCode值; 2.將hashCode的高16位參與運算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // 將(tab.length - 1) 與 hash值進行&運算 int index = (tab.length - 1) & hash;
HashMap底層數組的長度老是2的n次方,而且取模運算爲「h mod table.length」,對應上面的公式,能夠獲得該運算等同於「h & (table.length - 1)」。這是HashMap在速度上的優化,由於&比%具備更高的效率。數組
在JDK1.8的實現中,還優化了高位運算的算法,將hashCode的高16位與hashCode進行異或運算,主要是爲了在table的length較小的時候,讓高位也參與運算,而且不會有太大的開銷。安全
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // table不爲空 && table長度大於0 && table索引位置(根據hash值計算出)節點不爲空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // first的key等於傳入的key則返回first對象 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //first的key不等於傳入的key則說明是鏈表,向下遍歷 if ((e = first.next) != null) { // 判斷是否爲TreeNode,是則爲紅黑樹 // 若是是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //走下列步驟表示是鏈表,循環至節點的key與傳入的key值相等 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } }
//找不到符合的返回空 return null; }
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是否爲空或者length等於0, 若是是則調用resize方法進行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 經過hash值計算索引位置, 若是table表該索引位置節點爲空則新增一個 if ((p = tab[i = (n - 1) & hash]) == null) // 將索引位置的頭節點賦值給p tab[i] = newNode(hash, key, value, null); // table表該索引位置不爲空 else { //判斷p節點的hash值和key值是否跟傳入的hash值和key值相等 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若是相等, 則p節點即爲要查找的目標節點,賦值給e // 判斷p節點是否爲TreeNode, 若是是則調用紅黑樹的putTreeVal方法查找目標節點 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 走到這表明p節點爲普通鏈表節點 else { // 遍歷此鏈表, binCount用於統計節點數 for (int binCount = 0; ; ++binCount) { //p.next爲空表明目標節點不存在 if ((e = p.next) == null) { //新增一個節點插入鏈表尾部 p.next = newNode(hash, key, value, null); //若是節點數目超過8個,調用treeifyBin方法將該鏈表轉換爲紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //e節點的hash值和key值都與傳入的相等, 則e即爲目標節點,跳出循環 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // e不爲空則表明根據傳入的hash值和key值查找到了節點,將該節點的value覆蓋,返回oldValue if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用於LinkedHashMap return oldValue; } } //map修改次數加1 ++modCount; //map節點數加1,若是超過閥值,則擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 用於LinkedHashMap return null; }
從上面的源碼分析能夠看出數據結構
一、若是節點已經存在,則更新原值app
二、若是節點不存在,則插入數組中,若是數組已經有值,則判斷是非是紅黑樹,若是是,則調用紅黑樹方法插入函數
三、若是插入的是鏈表,插入尾部,而後判斷節點數是否超過8,若是超過,則轉換爲紅黑樹源碼分析
四、先插入的數據,後面判斷是否超過閥值再進行的擴容性能
putTreeVal,插入紅黑樹方法就不看了,看下treeifyBin方法,該方法是將鏈表轉化爲紅黑樹,優化
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // table爲空或者table的長度小於64, 進行擴容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 根據hash值計算索引值, 遍歷該索引位置的鏈表 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); // 鏈表節點轉紅黑樹節點 if (tl == null) // tl爲空表明爲第一次循環 hd = p; // 頭結點 else { p.prev = tl; // 當前節點的prev屬性設爲上一個節點 tl.next = p; // 上一個節點的next屬性設置爲當前節點 } tl = p; // tl賦值爲p, 在下一次循環中做爲上一個節點 } while ((e = e.next) != null); // e指向下一個節點 // 將table該索引位置賦值爲新轉的TreeNode的頭節點 if ((tab[index] = hd) != null) hd.treeify(tab); // 以頭結點爲根結點, 構建紅黑樹 }
}
能夠看到,會先判斷tab的節點數是否超過64,若是沒超過,則進行擴容,若是超過了纔會轉換爲紅黑樹
能夠獲得兩個結論
一、何時轉換爲紅黑樹
當鏈表數目超過8,而且map節點數量超過64,纔會轉換爲紅黑樹
二、何時擴容(前提是map數目沒有超過最大容量值 1<<30 )
新增節點時,發生了碰撞,而且節點數目超過閥值
新增節點時,發生了碰撞,節點數量木有超過閥值,可是鏈表數目>8,map節點<64時
再看下resize()方法
final Node<K,V>[] resize() { //oldTab保存未擴容的tab Node<K,V>[] oldTab = table; //oldTab最大容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldTab閥值 int oldThr = threshold; int newCap, newThr = 0; //若是老map有值 if (oldCap > 0) { // 老table的容量超過最大容量值,設置閾值爲Integer.MAX_VALUE,返回老表 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; //老table的容量沒有超過最大容量值,將新容量賦值爲老容量*2,若是新容量<最大容量而且老容量>=16, 則將新閾值設置爲原來的兩倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 老表的容量爲0, 老表的閾值大於0, 是由於初始容量被放入閾值 newCap = oldThr; // 則將新表的容量設置爲老表的閾值 else { //老表的容量爲0, 老表的閾值爲0, 則爲空表,設置默認容量和閾值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 若是新閾值爲空, 則經過新的容量*負載因子得到新閾值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 將當前閾值賦值爲剛計算出來的新的閾值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 將當前的表賦值爲新定義的表 // 若是老表不爲空, 則需遍歷將節點賦值給新表 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 將索引值爲j的老表頭節點賦值給e oldTab[j] = null; //將老表的節點設置爲空, 以便垃圾收集器回收空間 // 若是e.next爲空, 則表明老表的該位置只有1個節點, // 經過hash值計算新表的索引位置, 直接將該節點放在該位置 if (e.next == null) // newTab[e.hash & (newCap - 1)] = e; //e.next不爲空,判斷是不是紅黑樹 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //是普通鏈表 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; //若是e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置同樣 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //若是e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; // 最後一個節點的next設爲空 newTab[j] = loHead; // 將原索引位置的節點設置爲對應的頭結點 } if (hiTail != null) { hiTail.next = null; // 最後一個節點的next設爲空 newTab[j + oldCap] = hiHead; // 將索引位置爲原索引+oldCap的節點設置爲對應的頭結點 } } } } } return newTab; }
能夠看出,擴容時,節點重hash只分布在原索引位置與原索引+oldCap位置,爲何呢
假設老表的容量爲16,即oldCap=16,則新表容量爲16*2=32,假設節點1的hash值爲0000 0000 0000 0000 0000 1111 0000 1010,節點2的hash值爲0000 0000 0000 0000 0000 1111 0001 1010,則節點1和節點2在老表的索引位置計算以下圖計算1,因爲老表的長度限制,節點1和節點2的索引位置只取決於節點hash值的最後4位。再看計算2,計算2爲新表的索引計算,能夠知道若是兩個節點在老表的索引位置相同,則新表的索引位置只取決於節點hash值倒數第5位的值,而此位置的值恰好爲老表的容量值16,此時節點在新表的索引位置只有兩種狀況:原索引位置和原索引+oldCap位置(在此例中即爲10和10+16=26)。因爲結果只取決於節點hash值的倒數第5位,而此位置的值恰好爲老表的容量值16,所以此時新表的索引位置的計算能夠替換爲計算3,直接使用節點的hash值與老表的容量16進行位於運算,若是結果爲0則該節點在新表的索引位置爲原索引位置,不然該節點在新表的索引位置爲原索引+oldCap位置。
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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; // 若是table不爲空而且根據hash值計算出來的索引位置不爲空, 將該位置的節點賦值給p if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 若是p的hash值和key都與入參的相同, 則p即爲目標節點, 賦值給node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 不然向下遍歷節點 if (p instanceof TreeNode) // 若是p是TreeNode則調用紅黑樹的方法查找節點 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { // 遍歷鏈表查找符合條件的節點 // 當節點的hash值和key與傳入的相同,則該節點即爲目標節點 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; // 賦值給node, 並跳出循環 break; } p = e; // p節點賦值爲本次結束的e } while ((e = e.next) != null); // 指向像一個節點 } } // 若是node不爲空(即根據傳入key和hash值查找到目標節點),則進行移除操做 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) // 若是是TreeNode則調用紅黑樹的移除方法 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 走到這表明節點是普通鏈表節點 // 若是node是該索引位置的頭結點則直接將該索引位置的值賦值爲node的next節點 else if (node == p) tab[index] = node.next; // 不然將node的上一個節點的next屬性設置爲node的next節點, // 即將node節點移除, 將node的上下節點進行關聯(鏈表的移除) else p.next = node.next; ++modCount; // 修改次數+1 --size; // table的總節點數-1 afterNodeRemoval(node); // 供LinkedHashMap使用 return node; // 返回被移除的節點 } } return null; }
一、JDK1.7的時候使用的是數組+ 單鏈表的數據結構。可是在JDK1.8及以後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(n)變成O(logN)提升了效率)
二、JDK1.7用的是頭插法,而JDK1.8及以後使用的都是尾插法,那麼他們爲何要這樣作呢?由於JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法時會容易出現逆序且環形鏈表死循環問題。可是在JDK1.8以後是由於加入了紅黑樹使用尾插法,可以避免出現逆序且鏈表死循環的問題。
三、擴容後數據存儲位置的計算方式也不同:1. 在JDK1.7的時候是直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1),而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。
四、jdk1.7 先擴容再put ,jdk1.8 先put再擴容