HashMap是實現map接口的一個重要實現類,在咱們不管是平常仍是面試,以及工做中都是一個常常用到角色。它的結構以下:html
它的底層是用咱們的哈希表和紅黑樹組成的。因此咱們在學習HashMap底層原理的時候,須要有這兩種數據結構的知識作鋪墊,纔能有更好的理解!java
散列表是由咱們的數組和鏈表組成的,集成了兩種數據結構的優勢,咱們先簡單介紹一下這兩種數據結構。node
數組:數組存儲區間是連續的,佔用內存嚴重,故空間複雜度很大,但數組的二分查找時間複雜度很小,爲 o(1),數組的特色:查找速度快、插入和刪除效率低面試
鏈表:鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,爲 o(n),鏈表的特色:查找速度慢、插入和刪除效率高數組
哈希表:哈希表爲每一個對象計算出一個整數,稱爲哈希碼。根據這些計算出來的整數(哈希碼)保存在對應的位置上!若是遇到了哈希衝突,也就是同一個坑遇到了被佔用的狀況下,那麼咱們就會以鏈表的形式添加在後面。安全
關於紅黑樹的知識點比較多,若是過多介紹紅黑樹的話,那麼HashMap就很差介紹了。這裏給上一個鏈接,一篇關於紅黑樹很是好的文章。點擊這裏數據結構
好了,開始解析咱們的源碼,經過解析源碼更好的瞭解HashMap後,對那麼常見的面試題也能夠更加的吃透!多線程
首先就是介紹咱們的HashMap的基本屬性,對基本屬性介紹完以後,對後面方法裏使用時纔不會迷惑併發
一、咱們的默認的初始化的hashmap的容量,若是沒有指定的話,就是咱們的默認,1<<4就是16。app
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
二、咱們的hashmap最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
三、默認的裝載因子,0.75。有什麼用呢?好比咱們的容量如今是16,16*0.75=12,也就是說,當咱們的實際容量到了12的時候,那麼就會觸發擴容機制,進行擴容!
static final float DEFAULT_LOAD_FACTOR = 0.75f;
四、咱們知道哈希表是由數組和鏈表組成的,每個位置均可以說是一個哈希桶。咱們的哈希桶默認是鏈表,可是在JDK1.8以後咱們的哈希桶中當有TREEIFY_THRESHOLD個節點的時候,也就是下面默認的8,咱們桶中的鏈表會被轉換爲紅黑樹的結構。
static final int TREEIFY_THRESHOLD = 8;
五、與上面相同,不過不一樣的是,會將紅黑樹轉換成鏈表。
static final int UNTREEIFY_THRESHOLD = 6;
六、當哈希桶的結構轉換成樹以前,還會有一次判斷,只有鍵值對大於64纔會轉換!也就是咱們下面定義的最小容量,這是爲了不哈希表創建初期多個鍵值對恰巧都在一個哈希桶上面,而致使了不必的轉換。
static final int MIN_TREEIFY_CAPACITY = 64;
七、內部結構靜態內部類
八、其餘成員變量
這裏同時引起了咱們一些思考?爲何要將轉換成樹形結構的閾值設置爲8呢?爲何不將轉換成鏈表結構的閾值也設置爲8呢?這裏咱們在最後面試題分析的時候統一進行回答!
hashmap的構造方法有四個,不過咱們重點介紹其中的一個,由於這一個理解了,其餘的也不成問題。
//initialCapacity:初始大小 //loadFactor:裝載因子 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); }
總結了構造方法進行的操做:
tableSizeFor
來返回一個大於等於initialCapacity的2次冪。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; }
關於爲何作了位運算後能夠返回大於等於它的二次冪,能夠看一下這篇博文!點擊跳轉
這裏的threshold也就是咱們的閾值,當達到了這個閾值的時候咱們會進行擴容!可是這裏可能也會以爲疑惑,閾值不是容量*裝載因子嗎?不該該寫成下面這樣子嗎?
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
注意,在構造方法中,並無對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put方法中會用到resize()方法,而後對threshold從新計算。後面咱們對方法分析時會談到。
關於hashmap和核心方法和考點,其實都集中在put方法和resize()方法,這也會是咱們下面重點要介紹到的。
咱們首先來看put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
調用了咱們的putval方法,參入了一個以key計算的哈希值,key,value,還有兩個其餘參數。在看putVal方法以前先來看一下hash方法,看看它是如何計算哈希值。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
這是一個三目運算符,若是key不爲null的話,返回咱們的key的哈希值(低十六位)同時與高16位的異或運算。這一步的操做意義何爲呢?咱們先臨時跳到putVal方法裏面能夠看到有這麼一步操做
它將咱們計算出來的哈希值,與咱們的哈希表長度-1(爲了得到)進行&
運算,這是爲了獲取個人table下標。至於爲何-1呢?由於咱們的長度都是2的整數次冪,轉換成2進制也就是1000000....這種的形式,爲了更好的隨機,全部咱們進行了-1操做,也就是變成11111111這種。由於&
操做是都爲1的時候纔會爲1,因此個人的1多的時候隨機性纔會更大,畢竟一個1能幹過那麼多的1嗎?這是減小哈希衝突的第一步操做。舉個例子說明一下:
好比咱們的長度轉換爲2進製爲 1000 0000 ,進行-1操做後就是 0111 1111 而這個時候咱們原來的二進制數 1000 0000 & 0101 1011 = 0000 0000 與任何最高位不爲1的數進行&運算,都會變成0,也就讓咱們的哈希衝突變大了! 而咱們-1操做後 0111 1111 & 0101 1011 = 0101 1011 能夠看出來,這樣比原來的減小了不少的哈希衝突。 同時這也是爲何咱們要讓哈希的容量大小必定要爲2的整數次冪
好了,咱們要回答一下再上面那個問題了,爲何要返回低16位與高16位的異或做爲key的最終hash值呢?一樣舉個例子演示一下這個流程:
假設length爲8,HashMap的默認初始容量爲16;
length = 8 ,(length-1) = 7 , 轉換二進制爲111;
假設一個key的 hashcode = 78897121 ,轉換二進制:100101100111101111111100001,與(length-1)& 運算以下
0000 0100 1011 0011 1101 1111 1110 0001 &運算 0000 0000 0000 0000 0000 0000 0000 0111 = 0000 0000 0000 0000 0000 0000 0000 0001 (就是十進制1,因此下標爲1)
上述運算實質是:001 與 111 & 運算。也就是哈希值的低三位與length與運算。若是讓哈希值的低三位更加隨機,那麼&結果就更加隨機,就更能減小咱們的哈希衝突了。如何讓哈希值的低三位更加隨機,那麼就是讓其與高位異或,因此咱們纔在返回的時候與高位異或了再返回。低位與高位異或的過程舉個例子以下:
而後總結一下在與咱們與哈希值進行運算的時候有這麼一個規律:
當length=8時 下標運算結果取決於哈希值的低三位
當length=16時 下標運算結果取決於哈希值的低四位
當length=32時 下標運算結果取決於哈希值的低五位
當length=2的N次方, 下標運算結果取決於哈希值的低N位。
好了,咱們繼續回到咱們的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; //當咱們的table爲空的時候調用resize()進行擴容初始化 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)))) //hashcode和key相等,記錄下原先的值 e = p; //若是這個時候咱們的哈希桶已是紅黑樹結構,那麼調用樹的插入函數 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //鏈表結構,同時咱們的hashcode不相等 //找到與key相等的節點,更新value,退出循環 //若是沒有找到與key相等的節點,在鏈表尾部插入,若是插入後節點數量大於 //咱們變成紅黑樹的閾值,那麼進行轉換成紅黑樹 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; //空實現,爲LinkedHashMap預留 afterNodeAccess(e); return oldValue; } } ++modCount; //鍵值對達到閾值,進行擴容 if (++size > threshold) resize(); //空實現,爲LinkedHashMap預留 afterNodeInsertion(evict); return null; }
咱們在上面無論是源碼分析仍是在哪分析,都說到了咱們的resize()方法,下面咱們將正式開始講到
final Node<K,V>[] resize() { //原table數組賦值 Node<K,V>[] oldTab = table; //若是原數組爲null,那麼原數組長度爲0 int oldCap = (oldTab == null) ? 0 : oldTab.length; //賦值閾值 int oldThr = threshold; //newCap 新數組長度 //newThr 下次擴容的閾值 int newCap, newThr = 0; // 1. 若是原數組長度大於0 if (oldCap > 0) { //若是大於最大長度1 << 30 = 1073741824,那麼閾值賦值爲Integer.MAX_VALUE後直接返回 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 2. 若是原數組長度的2倍小於最大長度,而且原數組長度大於默認長度16,那麼新閾值爲原閾值的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 3. 若是原數組長度等於0,但原閾值大於0,那麼新的數組長度賦值爲原閾值大小 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 4. 若是原數組長度爲0,閾值爲0,那麼新數組長度,新閾值都初始化爲默認值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 5.若是新的閾值等於0 if (newThr == 0) { //計算臨時閾值 float ft = (float)newCap * loadFactor; //新數組長度小於最大長度,臨時閾值也小於最大長度,新閾值爲臨時閾值,不然是Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //計算出來的新閾值賦值給對象的閾值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //用新計算的數組長度新建一個Node數組,並賦值給對象的table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //後面是copy數組和鏈表數據邏輯 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; }
這個時候咱們以最初的三種構造方法來模擬一下流程。上面每個擴容狀況都標註了記號
//① Map<String, String> map = new HashMap<>(); map.put("1", "1"); //② Map<String, String> map1 = new HashMap<>(2); map1.put("2", "2"); //③ Map<String, String> map2 = new HashMap<>(2, 0.5f); map2.put("3", "3");
代碼4
邏輯,等到數組長度超過閾值12後,觸發第二次擴容,此時table數組,和threshold都不爲0,即oldTab、oldCap、oldThr都不爲0,先走代碼1
,若是oldCap長度的2倍沒有超過最大容量,而且oldCap 長度大於等於 默認容量16,那麼下次擴容的閾值 變爲oldThr大小的兩倍即 12 2 = 24,newThr = 24,newCap=32代碼3
,肯定此次擴容的新數組大小爲2,此時尚未肯定newThr 下次擴容的大小,因而進入代碼5
肯定newThr爲 2 0.75 = 1.5 取整 1 ,及下次擴容閾值爲1。當數組已有元素大於閾值及1時,觸發第二次擴容,此時oldCap爲1,oldThr爲1,走代碼1
newCap = oldCap << 1 結果爲 4 小於最大容量, 但oldCap 小於hashMap默認大小16,結果爲false,跳出判斷,此時因爲newThr等於0,進入代碼5
,肯定newThr爲 4 0.75 = 3,下次擴容閾值爲3代碼1
,同實例②,newCap = oldCap << 1 結果爲 4 小於最大容量, 但oldCap 小於hashMap默認大小16,結果爲false,跳出判斷,進入代碼5
,肯定newThr爲 4 * 0.5 = 2,下次擴容閾值爲2public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
獲取了咱們的key的hashcode而後做爲參數傳入getNode方法中!
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //若是計算出來的哈希值是在哈希表上 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //若是恰好在哈希桶的第一個節點上,返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //若是不在第一個節點,遍歷節點 if ((e = first.next) != null) { //若是這個時候咱們的哈希桶已是樹形結構了,調用樹形查找 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //對咱們的鏈表進行遍歷查找元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //若是沒有找到的話,返回null return null; }
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
首先是計算出咱們的hash,而後調用removeNode方法來移除
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; //咱們的哈希桶不爲空,同時要映射的哈希值也在 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 { //對鏈表進行查找key do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //找到了以後就去刪除,分成黑樹,桶的首位,鏈表中, 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) tab[index] = node.next; //在鏈表中,將下一個結點賦值給前一個節點的下一個節點,刪除自我 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
擴容是一個特別耗性能的操做,因此當咱們在使用 HashMap,正確估算 map 的大小,初始化的時候給一個大體的數值,避免 map 進行頻繁的擴容。
負載因子 loadFactor 是能夠修改的,也能夠大於1,可是建議不要輕易修改,除非狀況特殊。
HashMap 是非線程安全的,不要在併發的狀況下使用 HashMap,建議使用 ConcurrentHashMap!
關於HashMap的源碼就分析這些,由於這些足夠咱們去了解它的一些基本特性和常見面試足夠用了。下面我收集了一些面試題和咱們上面的留下的思考題進行分析!
一、爲何要將轉換成樹形結構的閾值設置爲8呢?爲何不將轉換成鏈表結構的閾值也設置爲8呢?
當初始閾值爲8時,鏈表的長度達到8的機率變的很小,若是再大機率減少的並不明顯
樹結構查找的時間複雜度是O(log(n)),而鏈表的時間複雜度是O(n),當閾值爲8時,long8 = 3,相比鏈表更快,但樹結構比鏈表佔用的空間更多,因此這是一種時間和空間的平衡
至於爲何不將轉換鏈表的閾值也設置爲8,是由於若是兩個值太接近的話,就會形成頻繁的轉換,致使咱們的時間複雜度變高。而在6是通過計算後最合適的數值
二、HashMap 爲何不用平衡樹,而用紅黑樹?
這一題應該歸類與數據結構了,不過這裏一樣給出分析
紅黑樹也是一種平衡樹,但不是嚴格平衡,平衡樹是左右子樹高度差不超過1,紅黑樹能夠是2倍
紅黑樹在插入、刪除的時候旋轉的機率比平衡樹低不少,效率比平衡樹高
查找時間複雜度都維持在O(logN),具體的還望查看紅黑樹的特性,上面最開始也給了一篇關於紅黑樹的介紹。
三、HashMap在併發下會產生什麼問題?有什麼替代方案?
HashMap併發下產生問題:因爲在發生hash衝突,插入鏈表的時候,多線程會形成環鏈,再get的時候變成死循環,Map.size()不許確,數據丟失。
關於爲何會形成環鏈的話,能夠看這裏!
替代方案:
四、HashMap中的key能夠是任何對象或數據類型嗎?
能夠是null,但不能是可變對象,若是是可變對象,對象中的屬性改變,則對象的HashCode也相應改變,致使下次沒法查找到已存在Map中的數據
若是要可變對象當着鍵,必須保證其HashCode在成員屬性改變的時候保持不變
五、爲何不直接將key做爲哈希值而是與高16位作異或運算?
這個咱們在上面說過了,還用圖和樣例解釋,是爲了更好的隨機性,解決哈希碰撞。
六、關於更多的面試題
這裏提供了一篇關於面試題挺多的博文,經過閱讀源碼,裏面大部分的面試題均可以解答了!
由於HashTable和HashMap非常相似,就跟咱們的Vector與ArrayList的關係同樣。提供了線程安全的解決方案,全部咱們在這裏經過區別,就至關與對HashTable進行了源碼分析!
從存儲結構和實現來說基本上都是相同的。
它和HashMap的最大的不一樣是它是線程安全的,另外它不容許key和value爲null。
Hashtable是個過期的集合類,不建議在新代碼中使用,不須要線程安全的場合能夠用HashMap替換,須要線程安全的場合能夠用ConcurrentHashMap替換或者Collections的synchronizedMap方法使HashMap具備線程安全的能力。
不一樣點 | HashMap | HashTable |
---|---|---|
數據結構 | 數組+鏈表+紅黑樹 | 數組+鏈表 |
繼承的類不一樣 | 繼承AbstractMap | 繼承Dictionary |
是否線程安全 | 否 | 是 |
性能高低 | 高 | 低 |
默認初始化容量 | 16 | 11 |
擴容方式不一樣 | 原始容量*2 | 原始容量*2+1 |
底層數組的容量爲2的整數次冪 | 要求爲2的整數次冪 | 不要求 |
確認key在數組中的索引的方法不一樣 | i = (n - 1) & hash; | index = (hash & 0x7FFFFFFF) % tab.length; |
遍歷方式 | Iterator(迭代器) | Iterator(迭代器)和Enumeration(枚舉器) |
Iterator遍歷數組順序 | 索引從小到大 | 索引從大到小 |
公衆號《Java3y》文章
知乎專欄《Java那些事兒》