Java源碼——HashMap的源碼分析及原理學習記錄
學習HashMap時,需要帶著這幾個問題去,會有很大的收獲:
一、什麽是哈希表
二、HashMap實現原理
三、為何HashMap的數組長度一定是2的次冪?
四、重寫equals方法需同時重寫hashCode方法
一.什麽是哈希表
在了解哈希表之前,先了解下其他數據結構的操作執行性能,數據結構的物理存儲結構只有兩種方式:順序存儲結構和鏈式存儲結構(棧,隊列,數,圖等)
數組:采用一段連續的存儲單元來存儲數據,對於指定下標的查找,時間復雜度為O(1);根據確定的值來查找,需要遍歷數組,逐一進行比較,時間復雜度為O(n)。對於有序數組,可以采用二分查找,插值查找,斐波那契查找等方式,復雜度為O(logn);對於一般的插入刪除操作。需要數組的移動,平均復雜度未O(n)。
線性表:對於鏈表的新增,刪除操作(在找到指定操作位置後),僅需要處理結點間的引用即可,時間復雜度為O(1),而查找操作需要遍歷鏈表逐一比較,復雜度為O(n)
二叉樹:對於一顆相對平衡的有序二叉樹,進行插入,查找沒刪除等操作,平均復雜度為O(logn)。
哈希表:在不考慮哈希沖突的情況下,僅僅一次定位就可以完成添加,刪除,查找等操作,時間復雜度為O(1),因為哈希表的主幹就是數組。
比如:需要新增或者查找某個元素,我們通過把當前元素的關鍵字通過某個函數映射到數組中的某個位置,通過數組下表一次定位就可以完成操作。
其中這個函數一般稱為哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。下圖為在哈希表中執行插入操作:
查找操作同理,先通過哈希函數計算出實際存儲地址,然後從數組中對應地址取出即可。
哈希沖突
如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同,換句話說,當對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現一家被其他元素占用了,這就是產生了哈希沖突,也叫哈希碰撞。哈希沖突的解決方法有多種:開放地址法(發生沖突,繼續尋找下一塊未被占用的存儲地址),再散列函數法,鏈地址法(jdk1.8之前HashMap采用該方法,也就是數組+鏈表的方式,jdk1.8當hash值的節點數不小於8時,采用數組+鏈表+紅黑樹)。
圖一表示jdk1.8之前的hashmap結構,左邊部分代表哈希表,也稱為哈希數組,數組的每個元素都是一個單鏈表的頭結點,鏈表是用來解決沖突的,如果不同的key映射到了數組的同一位置上,就將其放入鏈表中。所以當hash值相等的元素較多時,通過key依次在鏈表查找的效率較低,時間復雜度為O(n)。
圖二表示jdk1.8的hashmap的在同意hash值的節點數不小於8時的存儲結構,不再采用單鏈表形式存儲,而是采用紅黑樹。
二.HashMap的實現原理
HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對
//HashMap的主幹數組,可以看到就是一個Entry數組,初始值為空數組{},主幹數組的長度一定是2的次冪,至於為什麽這麽做,後面會有詳細分析。 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
(1)Entry是HashMap 的一個靜態內部類
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重復計算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
(2)在HashMap中重要的屬性字段
//table就是存儲Node類的數組,就是對應上圖中左邊那一欄, /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table; /** * The number of key-value mappings contained in this map. * 記錄hashmap中存儲鍵-值對的數量 */ transient int size; /** * hashmap結構被改變的次數,fail-fast機制,由於HashMap非線程安全,在對HashMap進行叠代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException */ transient int modCount; /** * The next size value at which to resize (capacity * load factor). * 擴容的門限值,當size大於這個值時,table數組進行擴容 */ int threshold; /** * The load factor for the hash table. * */ float loadFactor; /** * The default initial capacity - MUST be a power of two. * 默認初始化數組大小為16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 默認裝載因子, */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. * 這是鏈表的最大長度,當大於這個長度時,鏈表轉化為紅黑樹 */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64;
HashMap有4個構造器,其他構造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值
initialCapacity默認為16,loadFactory默認為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); this.loadFactor = loadFactor; //重新定義了擴容的門限 this.threshold = tableSizeFor(initialCapacity); } /** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; //先移位再或運算,最終保證返回值是2的整數冪 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ //當知道所要構建的數據容量的大小時,最好直接指定大小,提高效率 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //將map直接放入hashmap中 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } } /** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedMyHashMap for its Entry subclass.) */ 在hashMap的結構圖中,hash數組就是用Node型數組實現的,許多Node類通過next組成鏈表,key、value實際存儲在Node內部類中。 public 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; } }
(3)在HashMap中重要的方法,在常規構造器中,沒有為數組table分配內存空間(有一個入參為指定Map的構造器例外),而是在執行put操作的時候才真正構建table數組
/** * Associates the specified value with the specified key in thismap. * If the map previously contained a mapping for the key, the old * value is replaced. * */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; //key的值為null時,hash值返回0,對應的table數組中的位置是0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don‘t change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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賦給tab,判斷table是否為null或大小為0,若為真,就調用resize()初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //通過i = (n - 1) & hash得到table中的index值,若為null,則直接添加一個newNode if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //執行到這裏,說明發生碰撞,即tab[i]不為空,需要組成單鏈表或紅黑樹 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //此時p指的是table[i]中存儲的那個Node,如果待插入的節點中hash值和key值在p中已經存在,則將p賦給e e = p; //如果table數組中node類的hash、key的值與將要插入的Node的hash、key不吻合,就需要在這個node節點鏈表或者樹節點中查找。 else if (p instanceof TreeNode) //當p屬於紅黑樹結構時,則按照紅黑樹方式插入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //到這裏說明碰撞的節點以單鏈表形式存儲,for循環用來使單鏈表依次向後查找 for (int binCount = 0; ; ++binCount) { //將p的下一個節點賦給e,如果為null,創建一個新節點賦給p的下一個節點 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //如果沖突節點達到8個,調用treeifyBin(tab, hash),這個treeifyBin首先回去判斷當前hash表的長度,如果不足64的話,實際上就只進行resize,擴容table,如果已經達到64,那麽才會將沖突項存儲結構改為紅黑樹。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果有相同的hash和key,則退出循環 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//將p調整為下一個節點 } } //若e不為null,表示已經存在與待插入節點hash、key相同的節點,hashmap後插入的key值對應的value會覆蓋以前相同key值對應的value值,就是下面這塊代碼實現的 if (e != null) { // existing mapping for key V oldValue = e.value; //判斷是否修改已插入節點的value if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;//插入新節點後,hashmap的結構調整次數+1,保證並發訪問時,若HashMap內部結構發生變化,快速響應失敗 if (++size > threshold) resize();//HashMap中節點數+1,如果大於threshold,那麽要進行一次擴容 afterNodeInsertion(evict); return null; }
(4擴容函數resize()分析
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//定義臨時Node數組型變量,作為hash table //讀取hash table的長度 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold;//讀取擴容門限 int newCap, newThr = 0;//初始化新的table長度和門限值 if (oldCap > 0) { //執行到這裏,說明table已經初始化 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //二倍擴容,容量和門限值都加倍 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 //用構造器初始化了門限值,將門限值直接賦給新table容量 newCap = oldThr; else { // zero initial threshold signifies using defaults //老的table容量和門限值都為0,初始化新容量,新門限值,在調用hashmap()方式構造容器時,就采用這種方式初始化 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //如果門限值為0,重新設置門限 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;//更新新門限值為threshold @SuppressWarnings({"rawtypes","unchecked"}) //初始化新的table數組 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //當原來的table不為null時,需要將table[i]中的節點遷移 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //取出鏈表中第一個節點保存,若不為null,繼續下面操作 if ((e = oldTab[j]) != null) { oldTab[j] = null;//主動釋放 if (e.next == null) //鏈表中只有一個節點,沒有後續節點,則直接重新計算在新table中的index,並將此節點存儲到新table對應的index位置處 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //若e是紅黑樹節點,則按紅黑樹移動 ((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 { //下面這段暫時沒有太明白,通過e.hash & oldCap將鏈表分為兩隊,參考知乎上的一段解釋 /** * 把鏈表上的鍵值對按hash值分成lo和hi兩串,lo串的新索引位置與原先相同[原先位 * j],hi串的新索引位置為[原先位置j+oldCap]; * 鏈表的鍵值對加入lo還是hi串取決於 判斷條件if ((e.hash & oldCap) == 0),因為* capacity是2的冪,所以oldCap為10...0的二進制形式,若判斷條件為真,意味著 * oldCap為1的那位對應的hash位為0,對新索引的計算沒有影響(新索引 * =hash&(newCap-*1),newCap=oldCap<<2);若判斷條件為假,則 oldCap為1的那位* 對應的hash位為1, * 即新索引=hash&( newCap-1 )= hash&( (oldCap<<2) - 1),相當於多了10...0, * 即 oldCap * 例子: * 舊容量=16,二進制10000;新容量=32,二進制100000 * 舊索引的計算: * hash = xxxx xxxx xxxy xxxx * 舊容量-1 1111 * &運算 xxxx * 新索引的計算: * hash = xxxx xxxx xxxy xxxx * 新容量-1 1 1111 * &運算 y xxxx * 新索引 = 舊索引 + y0000,若判斷條件為真,則y=0(lo串索引不變),否則y=1(hi串 * 索引=舊索引+舊容量10000) */ 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; }
Java源碼——HashMap的源碼分析及原理學習記錄