在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即hash值相等的元素較多時,經過key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,可能會將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。html
首先存在一個table數組,裏面每一個元素都是一個node鏈表,當添加一個元素(key-value)時,就首先計算元素key的hash值,經過table的長度和key的hash值進行與運算獲得一個index,以此肯定插入數組中的位置,可是可能存在同一hash值的元素已經被放在數組同一位置了,這時就把這個元素添加到同一hash值的node鏈表的鏈尾,他們在數組的同一位置,可是造成了鏈表,同一各鏈表上的Hash值是相同的,因此說數組存放的是鏈表。而當鏈表長度大於等於8時,鏈表就可能轉換爲紅黑樹,這樣大大提升了查找的效率。java
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; } * * * }
transient Node<K,V>[] table;
// 序列化ID private static final long serialVersionUID = 362498820763181265L; // 初始化容量,初始化有16個桶 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量 1 073 741 824, 10億多 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的負載因子。所以初始狀況下,當鍵值對的數量大於 16 * 0.75 = 12 時,就會觸發擴容。 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當put()一個元素到某個桶,其鏈表長度達到8時有可能將鏈表轉換爲紅黑樹 static final int TREEIFY_THRESHOLD = 8; // 在hashMap擴容時,若是發現鏈表長度小於等於6,則會由紅黑樹從新退化爲鏈表。 static final int UNTREEIFY_THRESHOLD = 6; // 在轉變成紅黑樹樹以前,還會有一次判斷,只有鍵值對數量大於 64 纔會發生轉換,否者直接擴容。這是爲了不在HashMap創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。 static final int MIN_TREEIFY_CAPACITY = 64; // 存儲元素的數組 transient Node<k,v>[] table; // 存放元素的個數 transient int size; // 被修改的次數fast-fail機制 transient int modCount; // 臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容 int threshold; // 填充比 final float loadFactor;
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } 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; // tableSizeFor(initialCapacity)方法計算出接近initialCapacity // 參數的2^n來做爲初始化容量。 this.threshold = tableSizeFor(initialCapacity); } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
HashMap構造函數容許用戶傳入容量不是2的n次方,由於它能夠自動地將傳入的容量轉換爲2的n次方。node
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; // 「擾動函數」。參考 https://www.cnblogs.com/zhengwang/p/8136164.html return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; // 未初始化則初始化table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 經過table的長度和hash與運算獲得一個index, // 而後判斷table數組下標爲index處是否已經存在node。 if ((p = tab[i = (n - 1) & hash]) == null) // 若是table數組下標爲index處爲空則新建立一個node放在該處 tab[i] = newNode(hash, key, value, null); else { // 運行到這表明table數組下標爲index處已經存在node,即發生了碰撞 HashMap.Node<K,V> e; K k; // 檢查這個node的key是否跟插入的key是否相同。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 檢查這個node是否已是一個紅黑樹 else if (p instanceof TreeNode) // 若是這個node已是一個紅黑樹則繼續往樹種添加節點 e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { // 在這裏循環遍歷node鏈表 // 判斷是否到達鏈表尾 if ((e = p.next) == null) { // 到達鏈表尾,直接把新node插入鏈表,插入鏈表尾部,在jdk8以前是頭插法 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 若是node鏈表的長度大於等於8則可能把這個node轉換爲紅黑樹 treeifyBin(tab, hash); break; } // 檢查這個node的key是否跟插入的key是否相同。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 當插入key存在,則更新value值並返回舊value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // 修改次數++ ++modCount; // 若是當前大小大於門限,門限本來是初始容量*0.75 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
public V get(Object key) { HashMap.Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final HashMap.Node<K,V> getNode(int hash, Object key) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k; // table不爲空 if ((tab = table) != null && (n = tab.length) > 0 && // 經過table的長度和hash與運算獲得一個index,table // 下標位index處的元素不爲空,即元素爲node鏈表 (first = tab[(n - 1) & hash]) != null) { // 首先判斷node鏈表中中第一個節點 if (first.hash == hash && // always check first node // 分別判斷key爲null和key不爲null的狀況 ((k = first.key) == key || (key != null && key.equals(k)))) // key相等則返回第一個 return first; // 第一個節點key不一樣且node鏈表不止包含一個節點 if ((e = first.next) != null) { // 判斷node鏈表是否轉爲紅黑樹。 if (first instanceof HashMap.TreeNode) // 則在紅黑樹中進行查找。 return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key); do { // 循環遍歷node鏈表中的節點,判斷key是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // key在table中不存在則返回null。 return null; }
// 初始化或者擴容以後的元素調整 final HashMap.Node<K,V>[] resize() { // 獲取舊table HashMap.Node<K,V>[] oldTab = table; // 舊table容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 舊table擴容臨界值 int oldThr = threshold; // 定義新table容量和臨界值 int newCap, newThr = 0; // 若是原table不爲空 if (oldCap > 0) { // 若是table容量達到最大值,則修改臨界值爲Integer.MAX_VALUE // MAXIMUM_CAPACITY = 1 << 30; // Integer.MAX_VALUE = 1 << 31 - 1; if (oldCap >= MAXIMUM_CAPACITY) { // Map達到最大容量,這時還要向map中放數據,則直接設置臨界值爲整數的最大值 // 在容量沒有達到最大值以前不會再resize。 threshold = Integer.MAX_VALUE; // 結束操做 return oldTab; } // 下面就是擴容操做(2倍) else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 臨界值也變爲兩倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold /* * 進入此if證實建立HashMap時用的帶參構造:public HashMap(int initialCapacity) * 或 public HashMap(int initialCapacity, float loadFactor) * 注:帶參的構造中initialCapacity(初始容量值)不論是輸入幾都會經過 * tableSizeFor(initialCapacity)方法計算出接近initialCapacity * 參數的2^n來做爲初始化容量。 * 因此實際建立的容量並不等於設置的初始容量。 */ newCap = oldThr; else { // zero initial threshold signifies using defaults // 進入此if證實建立map時用的無參構造: // 而後將參數newCap(新的容量)、newThr(新的擴容閥界值)進行初始化 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 進入這表明有兩種可能。 // 1. 說明old table容量大於0可是小於16. // 2. 建立HashMap時用的帶參構造,根據loadFactor計算臨界值。 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 修改臨界值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 根據新的容量生成新的 table HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap]; // 替換成新的table table = newTab; // 若是oldTab不爲null說明是擴容,不然直接返回newTab if (oldTab != null) { /* 遍歷原來的table */ for (int j = 0; j < oldCap; ++j) { HashMap.Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 判斷這個桶(鏈表)中就只有一個節點 if (e.next == null) // 根據新的容量從新計算在table中的位置index,並把當前元素賦值給他。 newTab[e.hash & (newCap - 1)] = e; // 判斷這個鏈表是否已經轉爲紅黑樹 else if (e instanceof HashMap.TreeNode) // 在split函數中可能因爲紅黑樹的長度小於等於UNTREEIFY_THRESHOLD(6) // 則把紅黑樹從新轉爲鏈表 ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 運行到這裏證實桶中有多個節點。 HashMap.Node<K,V> loHead = null, loTail = null; HashMap.Node<K,V> hiHead = null, hiTail = null; HashMap.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; }