JDK1.8源碼學習-HashMaphtml
目錄java
1、HashMap簡介node
HashMap 主要用來存放鍵值對,它是基於哈希表的Map接口實現的,是經常使用的Java集合之一。數組
咱們都知道在JDK1.8 以前 的HashMap是 由 數組+鏈表 組成的,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的。JDK1.8 之後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲 8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。安全
爲何會有這種改變呢?數據結構
主要是由於以前HashMap在解決哈希衝突的時候默認是採用鏈表的方式,當出現哈希衝突時,以鏈表的方式來存儲衝突的數據,可是鏈表的查詢時間複雜度爲O(N),當鏈表過長時,就會發生查詢效率太低的問題。而 若是使用紅黑樹來存儲的話,那查詢時間複雜度直接降爲O(log(n)),這樣能夠解決鏈表查詢效率太低的問題,這就是爲何JDK1.8中的HashMap採用了鏈表和紅黑樹兩種方式,這點也能夠從下面的HashMap的數據結構中查看。併發
2、HashMap工做原理函數
咱們使用 put(key, value) 存儲對象到 HashMap 中,使用 get(key) 從 HashMap 中獲取對象。當咱們給 put() 方法傳遞鍵和值時,咱們先對鍵調用 hashCode() 方法,計算並返回的 hashCode 是用於找到 Map 數組的 bucket 位置來儲存 Node 對象。源碼分析
這裏關鍵點在於指出,HashMap 是在 bucket 中儲存鍵對象和值對象,做爲Map.Node 。學習
HashMap的初始化
Node[] table = new Node[16]; // 散列桶初始化,table class Node { hash; //hash值 key; //鍵 value; //值 node next; //用於指向鏈表的下一層(產生衝突,用拉鍊法) }
put過程
get過程
當咱們調用 get() 方法,HashMap 會使用鍵對象的 hashcode 找到 bucket 位置,找到 bucket 位置以後,會調用 keys.equals() 方法去找到鏈表中正確的節點,最終找到要找的值對象。
3、HashMap數據結構
上圖展現了HashMap(JDK1.8)的數據結構(數組+鏈表+紅黑樹),桶中的結構多是鏈表,也多是紅黑樹,紅黑樹的引入是爲了提升效率。
當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。
4、HashMap源碼分析
4.一、繼承關係分析
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap繼承自AbstractMap,實現了Map、Cloneable、Serializable接口,其中Map接口中定義了一些通用的操做,Cloneable接口可使HashMap調用clone()方法,進行淺層次的拷貝,Serializable接口可使HashMap實現序列化。
4.二、成員變量分析
//默認初始化map的容量:16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //左移運算,2的4次方 //map的最大容量:2^30 static final int MAXIMUM_CAPACITY = 1 << 30; //默認的填充因子:0.75,能較好的平衡時間與空間的消耗 static final float DEFAULT_LOAD_FACTOR = 0.75f; //將鏈表(桶)轉化成紅黑樹的臨界值 static final int TREEIFY_THRESHOLD = 8; //將紅黑樹轉成鏈表(桶)的臨界值 static final int UNTREEIFY_THRESHOLD = 6; //轉變成樹的table的最小容量,小於該值則不會進行樹化 static final int MIN_TREEIFY_CAPACITY = 64; //用來存儲數組,長度老是2的冪次 transient Node<K,V>[] table; //map中的鍵值對集合 transient Set<Map.Entry<K,V>> entrySet; //map中鍵值對的數量,即存儲節點的數量 transient int size; //用於統計map修改次數的計數器,用於fail-fast拋出ConcurrentModificationException transient int modCount; //擴展後數組的長度,大於該閾值,則從新進行擴容,threshold = capacity(table.length) * load factor int threshold; //負載因子,能夠進行指定,建議使用默認值0.75 final float loadFactor;
1.threshold
threshold = capacity(table.length) * load factor 當Size>=threshold的時候,就要考慮對數組的擴增了,這個值是衡量數組是否須要擴增的一個標準。
2.loadFactor負載因子
loadFactor負載因子是控制數組存放數據的疏密程度,loadFactor越趨於1,那麼數組中存放的數據也就越多,也就越密,鏈表也會越長,loadFactor越小,也就是趨近於0。
當loadfactor太大則會致使查找元素的效率低下,過小則會致使數組的利用率低,存放的數據會很分散。loadFactor的默認值爲0.75f是官方給出的一個較好的臨界值。
HashMap中是使用Node[]數組來存儲數據的,每個Node都指向下一個節點,採用的是鏈表結構。
// 繼承自 Map.Entry<K,V> static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 哈希值,存放元素到hashmap中時用來與其餘元素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; } // 重寫hashCode()方法 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 重寫 equals() 方法 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; } }
4.三、構造函數分析
4.3.1無參構造函數
只是初始化了負載因子,並無初始化數組的大小。
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; //採用默認的0.75f }
4.3.2傳入初始化容量構造函數(若是初始化時知道HashMap的容量大小,建議採用此種構造函數)
指定初始化數組的大小。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
4.3.3傳入初始化容量及填充因子構造函數
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); }
4.3.4包含另外一個Map的構造函數
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
tableSizeFor()方法:返回一個最小的且比用戶給定參數(cap)大的或者等於的,而且是2的整數次冪的數值。
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; 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; }
putMapEntries()方法:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //獲取m中鍵值對的數量 int s = m.size(); if (s > 0) { //判斷table是否已經初始化 if (table == null) { //計算map的容量,鍵值對的數量 = 容量 * 填充因子 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //若是容量大於了閾值,則從新計算閾值。 if (t > threshold) threshold = tableSizeFor(t); } //若是table已經有,且鍵值對數量大於了閾值,進行擴容處理 else if (s > threshold) resize(); //將m中全部元素添加至HashMap中 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); } } }
4.四、put()方法分析
HashMap只提供了put()方法用於添加元素,在put()方法中調用了putVal()方法,這個方法沒有提供給用戶。
putVal()方法:
1.若是定位到的數組位置沒有元素則直接插入新的元素。
2.若是定位到的數組位置有元素就要和插入的key進行比較,若是key值相同就直接覆蓋,若是key值不相同,則判斷p是不是一個樹節點,若是是就將元素添加進去,不然遍歷鏈表進行插入數據。
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,則擴容。注意這裏的賦值操做,關係到下面 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(n - 1) & hash肯定了元素放在哪一個桶裏面,若是tab對應的數組位置爲空,則建立新的node,並指向它 if ((p = tab[i = (n - 1) & hash]) == null) // newNode方法就是返回Node:return new Node<>(hash, key, value, next); tab[i] = newNode(hash, key, value, null); else {//桶中已經存在元素 Node<K,V> e; K k; //若是比較hash值和key的值都相等,說明要put的鍵值對已經在裏面,賦值給e if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //賦值,用e來進行記錄 e = p; //若是p節點是紅黑樹節點,則執行插入樹的操做(hash值不相等,即key值不相等) else if (p instanceof TreeNode) //放入樹中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//若是是鏈表節點 for (int binCount = 0; ; ++binCount) { //找到了最後一個都不知足的話,則在鏈表最後插入節點。注意這裏的e = p.next,賦值兼具判斷都在if裏了 if ((e = p.next) == null) //在末尾插入新節點 p.next = newNode(hash, key, value, null); //以前field說明中的,若是節點的數量大於樹化閾值,則轉化成紅黑樹,第一個是-1 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } //判斷鏈表中節點的key值與插入元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //相等則跳出循環 break; //遍歷桶中的鏈表,與前面的e=p.next組合,能夠遍歷鏈表 p = e; } } //上面循環中找到了e,則根據onlyIfAbsent是否爲true來決定是否替換舊值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //鉤子函數,用於給LinkedHashMap繼承後使用,在HashMap裏是空的 afterNodeAccess(e); return oldValue; } } //修改計數器+1 ++modCount; //實際大小+1, 若是大於閾值,從新計算並擴容 if (++size > threshold) resize(); //鉤子函數,用於給LinkedHashMap繼承後使用,在HashMap裏是空的 afterNodeInsertion(evict); return null; }
4.五、get()方法分析
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; //先是判斷一通table是否爲空以及根據hash找到存放的table數組的下標,並賦值給臨時變量 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //老是先檢查數組下標第一個節點是否知足key,知足則返回 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; //若是第一個與key不相等,則循環查看桶 if ((e = first.next) != null) { //檢查是否爲樹節點,是的話採用樹節點的方法來獲取對應的key的值 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //do-while循環判斷,在鏈表中進行查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
4.六、resize()方法分析
HashMap的擴容方法,會伴隨着一次新的hash分配,而且會遍歷hash表中全部的元素,是很是耗時的。在HashMap的使用過程當中儘可能避免resize。
擴容的過程是依據存放的節點(Node)數量是否超過閾值來判斷的,若是超過閾值則擴容一倍(即擴充爲當前閾值的2倍)。
final Node<K,V>[] resize() { //獲取舊的table,cap,threshold //若是數組爲空,則會建立一個默認容量爲16的數組,threshold爲12 Node<K,V>[] oldTab = table; //擴容/縮容前的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //舊的閾值 int oldThr = threshold; int newCap, newThr = 0; //說明以前已經初始化過map if (oldCap > 0) { //達到了最大的容量,則將閾值設爲最大,而且返回舊的table(此時超過了最大值再也不進行擴容,進行隨機碰撞) if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //若是兩倍的舊容量小於最大的容量(即沒有超過最大值)且舊容量大於等於默認初始化容量,則舊的閾值也擴大兩倍。 //oldCap << 1,其實就是*2的意思,擴容至閾值的2倍。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //舊容量爲0且舊閾值大於0,則賦值給新的容量(應該是針對初始化的時候指定了其容量的構造函數出現的這種狀況) else if (oldThr > 0) newCap = oldThr; //這種狀況就是調用無參數的構造函數 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新閾值爲0,則經過:新容量*填充因子 來計算resize的上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //根據新的容量來初始化table,並賦值給table @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //若是舊的table裏面有存放節點,則初始化給新的table(即將bucket移動到新的buckets中) if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //將下標爲j的數組賦給臨時節點e if ((e = oldTab[j]) != null) { //清空 oldTab[j] = null; //若是e.next爲null,說明當前節點只有一個值,則直接經過計算hash和新的容量來肯定新的下標,更新當前值到newTab便可 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若是爲樹節點,按照樹節點的來拆分 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //e還有其餘的節點,將該桶拆分紅兩份(不必定均分) else { //loHead是拆分後的,鏈表的頭部,tail爲尾部,以鏈表的方式來逐個添加數據到newTab 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來拆分,注意以前是 e.hash & (oldCap - 1) 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; }
4.七、remove()方法分析
public V remove(Object key) { Node<K,V> e; //與以前的put、get同樣,remove也是調用其餘的方法(removeNode方法) 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是否爲空之類的邏輯,根據key和hashCode來獲取對應的索引的位置 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; //對下標節點進行判斷,若是相同,則賦給臨時節點 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) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else {//按照鏈表的方式進行節點遍歷 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //若是找到了key對應的node,則進行刪除操做 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //爲樹節點,則進行樹節點的刪除操做 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //爲鏈表節點,進行鏈表節點的刪除操做 else if (node == p) //若是p == node,說明該key所在的位置爲數組的下標位置,因此下標位置指向下一個節點便可 tab[index] = node.next; //不然的話,key在桶中,p爲node的上一個節點,p.next指向node.next便可 else p.next = node.next; //修改計數器 ++modCount; --size; //鉤子函數,與上同 afterNodeRemoval(node); return node; } } return null; }
這裏須要注意的是在進行是不是同一個節點進行判斷的時候使用的是(p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))的方式,即首先判斷兩個元素的hash值是否相等,若是相等纔會使用equals()方法進行比較,不然就說明這兩個元素必定不是同一個對象,直接返回。若是hash值是同樣的,則進行equals()判斷key值,兩個條件都成立時,認定兩個元素是同一個值。
因此咱們在修改對象的equals()方法的時候,也須要對hashCode()方法進行修改,若是不修改的話hash值可能相等,equal()方法也可能相等,同時成立的話會被認爲是同一對象,直接進行覆蓋操做。
使用remove()方法最多見的 java.util.ConcurrentModificationException異常,舉例
Map<String, Integer> map = new HashMap<>(); map.put("GoddessY", 1); map.put("Joemsu", 2); for (String a : map.keySet()) { if ("GoddessY".equals(a)) { map.remove(a); } }
拋出異常的源碼
public Set<K> keySet() { Set<K> ks; return (ks = keySet) == null ? (keySet = new KeySet()) : ks; } final class KeySet extends AbstractSet<K> { public final Iterator<K> iterator() { return new KeyIterator(); } } final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } abstract class HashIterator { //指向下一個節點 Node<K,V> next; //指向當前節點 Node<K,V> current; //迭代前的修改次數 int expectedModCount; //當前下標 int index; HashIterator() { //注意這裏:將修改計數器值賦給expectedModCount expectedModCount = modCount; //下面一頓初始化。。。 Node<K,V>[] t = table; current = next = null; index = 0; //在table數組中找到第一個下標不爲空的節點。 if (t != null && size > 0) { do {} while (index < t.length && (next = t[index++]) == null); } } //經過判斷next是否爲空,來決定是否hasNext() public final boolean hasNext() { return next != null; } //這裏就是拋出ConcurrentModificationException的地方 final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; //若是modCount與初始化傳進去的modCount不一樣,則拋出併發修改的異常 if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); //若是一個下標對應的桶空了,則接着在數組裏找其餘下標不爲空的桶,同時賦值給next if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } //使用迭代器的remove不會拋出ConcurrentModificationException異常,緣由以下: public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); //注意這裏:對expectedModCount從新進行了賦值。因此下次比較的時候仍是相同的 expectedModCount = modCount; } }
具體的詳細緣由和解決方法有興趣的園友能夠直接搜索concurrentmodificationexception異常,這裏給出一篇參考地址: https://www.cnblogs.com/snowater/p/8024776.html
5、HashMap總結
一、非線程安全,無序,能夠有一個key值爲null或多個value爲null。
二、默認大小是16,擴充爲2的指數。
三、最好可以在初始化HashMap的時候指定其容量,這樣能使效率比使其存儲空間不夠後自動增加更高。
四、除了使用迭代器的remove方法外使用其餘方式刪除,都會拋出ConcurrentModificationException。
參考連接
https://www.cnblogs.com/joemsu/p/7724623.html
http://www.importnew.com/31096.html
http://www.importnew.com/31278.html
https://tech.meituan.com/2016/06/24/java-hashmap.html
http://www.cnblogs.com/leesf456/p/5242233.html
http://www.pianshen.com/article/6104166010/