單純分析和學習hashmap的實現,很少說與Hashtable、ConcurrentHashMap等的區別。java
基於 jdk1.8node
在面試中有些水平的公司比較喜歡問HashMap原理,其中涉及的點比較多,並且大多能造成連環炮形式的問題。程序員
通常連環炮,一環不知道後面試官也就不問了,可是低層連環沒連上,恭喜扣分是大大的,連到比較深的時候,說不知道還好點,好比:面試
1.1Hashmap是否是有序的? 不是繼續算法
1.2有沒有有順序的Map? TreeMap LinkedHashMap數組
1.3它們是怎麼來保證順序的? 通常都要說到其源碼,要不說不清爲麼有序安全
1.4答兩個有序或以上的 繼續 你以爲它們有序的區別,那個比較好,在什麼場景用哪一個好?數據結構
1.4答一個也能夠問上面的場景 繼續併發
1.5你以爲有沒有更好或者更高效的實現方式?有app
1.6 答有 這個時候提及來可能就要跑到底層數據結構上去了
數據結構繼續衍生 到 算法等等。。。
就這一個遇到大佬問你,能把不少人連到懷疑人生
2.關於hash的
1.1 hashmap基本的節點結構? Node 鍵值對
1.2 鍵是什麼樣的,我用字符串a那鍵就是a嘛? 不是會進行hash
1.3 如何hash的 這樣hash有什麼好處? 源碼hashmap的hash算法
1.4 Hash在java中主要做用是什麼?
1.5 Hashcode equal相關 須要同時重寫?緣由?
1.6 equal引出的對象地址、string帶有字符串緩衝區、字符串常量池
等等。。。
3.關於線程安全問題、到concurrent包等
前面說這些就是想說,hashmap中用到的東西不少,深刻學習和理解對每一個想晉升的程序員來講基本是必須,同時由它引出的對比,也是無限多,有很大的必要學習。
1.只有一些靜態屬性會進行賦值,具體每一個值什麼用,暫時無論
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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;
2.沒有靜態的代碼塊,不會直接運行
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } 1.AbstractMap父類,構造方法也沒幹事不談 2.只是賦值loadFactor 0.75f 沒幹別的事 3.static final float DEFAULT_LOAD_FACTOR = 0.75f; 4.loadFactor屬性 做用先放着後面用到再看 5.沒幹別的事了
先看經常使用的put鍵值對,這個學完了,那麼其餘的put方法就沒什麼問題了,好比putAll、putIfAbsent、putMapEntries
同時put弄明白了 取值就是一個反向就簡單了
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
1.先對key進行hash計算,學一下
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 1.1 看出key是能夠空的 hash爲0 1.2 (h = key.hashCode()) ^ (h >>> 16) 第一步取key的hashcode值 關於更底層的hashcode是什麼 有興趣再看 h ^ (h >>> 16) 第二步 高位參與運算 這個hash值的重要性就不說了,這裏這麼幹是出於性能考慮,底層的移位和異或運算確定比加減乘除取模等效率好 hashcode是32位的,無符號右移16位,那生成的就是16位0加原高位的16位值, 就是對半了,異或計算也就變成了高16位和低16位進行異或,原高16位不變。這麼幹主要用於當hashmap 數組比較小的時候全部bit都參與運算了,防止hash衝突太大, 所謂hash衝突是指不一樣的key計算出的hash是同樣的,好比a和97,這個確定是存在的沒毛病
/** * 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 相同key是否是覆蓋值 * @param evict if false, the table is in creation mode. 在hashmap中沒用 * @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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { 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); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 2.1 執行順序 第一句 Node<K,V>[] tab; Node<K,V> p; int n, i; 申明變量 Node是啥,學習一下: 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個值,hash、key、value和另外一個Node對象引用的HashMap子元素結構,即咱們裝的每一個鍵值對就用一個Node對象存放 第二句 if ((tab = table) == null || (n = tab.length) == 0) 這句 tab = table賦值,table如今是null的,so n = tab.length不運行了 運行這個if的代碼塊 第三句 n = (tab = resize()).length; 從下面的執行知道 n=16 調用resize(),返回Node數組,這個resize是一個很是重要的方法,咱們就依如今的對象狀態去看這個方法,不帶入其餘狀態,認真研究學習下 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { 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 newCap = oldThr; else { // zero initial threshold signifies using defaults 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) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; 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; 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; } resize 1.Node<K,V>[] oldTab = table; 在上面知道table是null的,so oldTab也是null 2.int oldCap = (oldTab == null) ? 0 : oldTab.length; oldCap=0 3.int oldThr = threshold; threshold咱們沒賦值過,int初始0 , oldThr=threshold=0 4.int newCap, newThr = 0; 不談 5.if (oldCap > 0) { oldCap=0 if不運行 6.else if (oldThr > 0) oldThr=0 if也不運行 7.else { newCap = DEFAULT_INITIAL_CAPACITY; DEFAULT_INITIAL_CAPACITY靜態成員變量,初始 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 so newCap=16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); static final float DEFAULT_LOAD_FACTOR = 0.75f; 0.75*16=12 newThr=12 } 8. if (newThr == 0) { newThr=12 if不運行 9. threshold = newThr; threshold = newThr=12 10. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap] 申明一個16個大小的Node數組 11. table = newTab; 看出來了吧,table是成員變量,也就代表,HashMap初始數據結構是一個16的Node數組 12. if (oldTab != null) { oldTab是1中賦值的null,if不運行 13. return newTab; 返回16大小的node數組 總結,這一波調用是初次調用其實沒幹別的事,就是定義了基本的數據結構是16個Node數組,可是這個方法不簡單,由於一些if沒走 第四句 if ((p = tab[i = (n - 1) & hash]) == null) n=16 15&hash 結果確定是0-15,這裏就看出,這是在計算一個key應該在整個數據結構16的數組中的索引了,並賦值給i變量,後面無論總體結構n變多大,這種計算key所在的索引是很是棒的設計。 如今的狀態是初始的 確定是null的吧 if運行 第五句 tab[i] = newNode(hash, key, value, null); new一個節點Node,放在數組裏,i是第四句計算的索引 第六句 else { 不運行 第七句 ++modCount; transient int modCount; 根據註釋能夠看出,這個是記錄數據結構變更次數的,put值確定是變了的 第八句 if (++size > threshold) size=1 threshold在調用resize時賦值12 if不運行 第九句 afterNodeInsertion(evict); 沒幹事 第十句 return null; 不談
3.putVal 再回頭詳走,第一遍幹了不少初始化的事有些東西還沒研究到
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { 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); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 第一句 Node<K,V>[] tab; Node<K,V> p; int n, i; 申明變量不談 第二句 if ((tab = table) == null || (n = tab.length) == 0) 這句 tab = table賦值,table如今是16數組 n=16 if不運行 第三句 if ((p = tab[i = (n - 1) & hash]) == null) 再看就知道了判斷當前存的key計算出的索引位置是否是已經存過值了 沒存過就新Node存 和上面一遍同樣 咱們當已經有值了 有值其實就意味着發生hash衝突了 好比key分別是a和97 hashCode都是97 衝突 所以此次咱們主要看下一個else裏面HashMap是怎麼處理衝突的 第四句 else中內容 即衝突處理 p是衝突時數組該索引位置的元素 1. p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))) 判斷新元素hash和key是否是都和p相同,相同表示存了同樣的key 直接賦值給e 2. p instanceof TreeNode(紅黑樹,具體的紅黑樹算法這裏就不詳細寫了,有興趣能夠去學習) 怎麼猛然來個紅黑樹,再3裏說 判斷原來元素是否是 TreeNode 類型 TreeNode同樣是靜態內部類,再看看就是紅黑樹的節點,所以這個地方用到了紅黑樹 putTreeVal 向紅黑樹中添加元素 內部實現,存在相同key就返回賦值給e 不存在就添加並返回null 源碼就是紅黑樹算法 3.key不一樣也不是紅黑樹 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); } 先不看再裏面的那個if,這個一看就知道了吧,明顯的鏈表啊,並且數據裏的這個元素是鏈表頭 整個循環,明顯是在從頭開始遍歷鏈表,找到相同key或鏈表找完了新元素掛鏈表最後 但在其中還有這麼個if if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; 這是在鏈表找完了,且新元素已經掛在鏈表最後了有的一個判斷 判斷循環次數,其實就是鏈表長度,長度超過TREEIFY_THRESHOLD 默認8則運行treeifyBin(tab, hash); 就是這個方法把鏈表變成紅黑樹了,具體方法源碼不談了,學紅黑樹就能夠了 最後判斷e是否是空,上面的衝突方案看出e不是空就是表示有相同的key進行value覆蓋就能夠,e空就是無相同key且完成了數據掛載 總結此次再走一遍putVal就是爲了學習HashMap的衝突處理方案,也看出內存結構是數組、鏈表、紅黑樹組成的,紅黑樹是java8新引進,是基於性能的考慮,在衝突大時,紅黑樹算法會比鏈表綜合表現更好
4.resize 再詳走 putVal最後一段size>threshold threshold初始12 ++size元素數量確定會有超12個的時候,這裏也就看出了threshold表明HashMap的容量,到上限就要擴容了,默認如今16數組,12元素上限
1.Node<K,V>[] oldTab = table; 16大小 2.int oldCap = (oldTab == null) ? 0 : oldTab.length; oldCap=16 3.int oldThr = threshold; 12 4.int newCap, newThr = 0; 不談 5.if (oldCap > 0) { oldCap=16運行 oldCap是總體結構數組大小 if (oldCap >= MAXIMUM_CAPACITY) { 判斷數組大小是否是已經到上限1<<30 threshold = Integer.MAX_VALUE; 到達上線 threshold 賦值最大值 而後返回 表示以後就再也不幹別的事了,隨便存,隨便hash衝突去,就這麼大,無限增長紅黑樹節點了 return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) 賦值newCap爲2倍數組大小,判斷若是擴充2倍有沒到上限,且不擴充時容量是否大於默認的16 newThr = oldThr << 1; // double threshold 知足則賦值 容量改成24 } 這段看出到threshold容量了就進行2倍擴容 6.if (newThr == 0) { 若是運行該if 0 表示5步中擴容2倍到上限或原數組大小小於16 float ft = (float)newCap * loadFactor; newCap如今是2倍原大小的*0.75 2倍數組大小時的容量 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); 判斷2倍數組大小和2倍後的容量是否是都小於最高值,是則賦值新容量,不是就用整形最大值 } 7. threshold = newThr; 把5 6兩步算出的新容量賦值給HashMap 也說明要擴容了 8. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 和後面的循環主要就是把原數組中的元素,一個一個添加到新數組中,轉移的一個過程 總結,這一波調用是瞭解HashMap的擴容方式,看下來就是2倍擴容直到上限
5.總結,到這put就比較詳細了,也知道了基本結構是數組、鏈表、紅黑樹,鏈表到8個時轉換成紅黑樹
同時每次進行2倍擴容和數據轉移,擴容是用新結構的那顯然減小擴容次數會有更好的性能
那就要求每次聲明HashMap時最好是指定大小的
3、一些其餘咱們須要知道的
1.指定大小的初始化
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; this.threshold = tableSizeFor(initialCapacity); } 第一個經常使用,第二個建議是不用,不去動0.75的這個容量比例,固然不絕對 這裏tableSizeFor是一個很神奇的算法,我很是佩服的一個算法 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; } 這個方法是在找大於等於cap且最小2的冪 好比cap=1 結果 2 0次方 1 cap=2 2 cap=3 4 cap=9 16 分析下等於9 cap - 1 第一步結果8 00000000000000000000000000001000 8 00000000000000000000000000000100 右移1位 00000000000000000000000000001100 或運算 結果 00000000000000000000000000000011 右移2位 00000000000000000000000000001111 或運算 結果 00000000000000000000000000001111 右移 4 8 16沒用全是0結果仍是這個15 最終 +1 16 分析下等於大點 12345678 00000000101111000110000101001110 12345678 00000000101111000110000101001101 -1結果 12345677 00000000010111100011000010100110 右移1位 00000000111111100111000111101111 或運算 結果 00000000001111111001110001111011 右移2位 00000000111111111111110111111111 差很少了在移0就沒了都是1了,+1不是確定是2的倍數了 再說開始-1緣由這是爲了防止,cap已是2的冪。 若是cap已是2的冪, 又沒有執行這個減1操做,則執行完後面的幾條無符號右移操做以後,返回的capacity將是這個cap的2倍。若是不懂,要看完後面的幾個無符號右移以後再回來看看
2.HashMap數組結構爲何用2的倍數 高速的索引計算,使用HashMap確定是衝突越少越好,就要求分部均勻,最好的用取模 h % length,可是近一步若是用2的冪h & (length - 1) == h % length 是等價的,效率缺差卻別很是大 綜合衡量用空間換了時間,且是值得的 3.線程安全問題 線程不安全,就put來看全程沒考慮線程問題,確定不安全,如今隨便併發一下resize會混亂吧,put鏈表,紅黑樹掛載基本都會出問題