聲明:本文以jdk1.8爲主!java
做爲一個Java從業者,面試的時候確定會被問到過HashMap,由於對於HashMap來講,能夠說是Java==集合中的精髓==了,若是你以爲本身對它掌握的還不夠好,我想今天這篇文章會很是適合你,至少,看了今天這篇文章,之後不怕面試被問HashMap了web
其實在我學習HashMap的過程當中,我我的以爲HashMap仍是挺複雜的,若是真的想把它搞得明明白白的,沒有足夠的內力怕是一時半會兒作不到,不過咱們總歸是在不斷的學習,所以真的沒必要強迫本身把如今遇到的一些知識點所有搞懂。面試
可是,對於HashMap來講,你所掌握的應該足夠可讓你應對面試,因此今天我們的側重點就是學會那些常常被問到的知識點。算法
我猜,你確定看過很多分析HashMap的文章了,那麼你掌握多少了呢?從一個問題開始吧數組
怎麼樣,想要回答這個問題,仍是須要你對HashMap有個比較深刻的瞭解的,若是僅僅知道什麼key和value的話,那麼回答這個問題就比較難了。微信
這個問題你們能夠先想一想,後面我會給出解答,下面咱們一步步的來看HashMap中幾個你必須知道的知識點。數據結構
HashMap隸屬於Java中集合這一塊,咱們知道集合這塊有list,set和map,這裏的HashMap就是Map的實現類,那麼在Map這個你們族中還有哪些重要角色呢?多線程
TreeMap從名字上就能看出來是與樹有關,它是基於樹的實現,而HashMap,HashTable和ConcurrentHashMap都是基於hash表的實現,另外這裏的HashTable和HashMap在代碼實現上,基本上是同樣的,還記得以前在講解ArrayList的時候提到過和Vector的區別嘛?這裏他們是很類似的,通常都不怎麼用HashTable,會用ConcurrentHashMap來代替,這個也須要好好研究,它比HashTable性能更好,它的鎖粒度更小。app
因爲這不是本文的重點,只作簡單說明,後續會發文單獨介紹。函數
簡單來講,Map就是一個映射關係的數據集合,就是咱們常見的k-v的形式,一個key對應一個value,大體有這樣的圖示
上面簡單提到過,HashMap是基於Hash表的實現,所以,瞭解了什麼是Hash表,那對學習HashMap是至關重要。
以前特地寫了一篇介紹哈希表的,不瞭解的趕忙去看看:來吧!一文完全搞定哈希表!
建議瞭解了哈希表以後再學習HashMap,這樣不少難懂的也就不那麼難理解了。
接着,HashMap是基於hash表的實現,而說到底,它也是用來存儲數據供咱們使用的,那麼底層是用什麼來存儲數據的呢?可能有人猜到了,仍是數組,爲啥仍是數組?想一想以前的ArrayList,怎麼,對ArrayList也不瞭解。
沒事,恰好我也寫了一篇:掌握這些,ArrayList就不用擔憂了!
因此,對於HashMap來講,底層也是基於數組實現,只不過這個數組可能和你印象中的數組有些許不一樣,咱們日常整個數組出來,裏面會放一些數據,好比基礎數據類型或者引用數據類型,數組中的每一個元素咱們沒啥特殊的叫法。
可是在HashMap中人家就有了新名字,我發現這個知識點其實不少人都不太清楚:
在HashMap中的底層數組中,每一個元素在jdk1.7及以前叫作Entry,而在jdk1.8以後人家又更名叫作Node。
這裏可能仍是會有人好奇這Entry和Node長啥樣,這個看看源碼就比較清楚了,後面咱們會說。
到了這裏你因該就能簡單的理解啥是HashMap了,若是你看過什麼是哈希表了,你就會清楚,在HashMap中一樣會出現哈希表所描述的那些問題,好比:
沒事,這些問題咱們後續都會談到。
先來看HashMap的基礎用法:
HashMap map = new HashMap();
複製代碼
就這樣,咱們建立好了一個HashMap,接下來咱們看看new以後發生了什麼,看看這個無參構造函數吧
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
複製代碼
解釋下新面孔:
很簡單,當你新建一個HashMap的時候,人家就是簡單的去初始化一個負載因子,不過咱們這裏想知道的是底層數組默認是多少嘞,顯然咱們沒有獲得咱們的答案,咱們繼續看源碼。
在此以前,想一下以前ArrayList的初始化大小,是否是在add的時候才建立默認數組,這裏會不會也同樣,那咱們看看HashMap的添加元素的方法,這裏是put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
複製代碼
這裏大眼一看,有兩個方法;
這裏須要再明確下,這是咱們往HashMap中添加第一個元素的時候,也就是第一次調用這個put方法,能夠猜測,如今數據已通過來了,底層是否是要作存儲操做,那確定要弄個數組出來啊,好,離咱們想要的結果愈來愈近了。
先看這個hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
記得以前聊哈希表的時候說過,哈希表的數據存儲有個很明顯的特色,就是根據你的key使用哈希算法計算得出一個下標值,對吧,不懂得趕忙看:來吧!一文完全搞定哈希表!
而這裏的hash就是根據key獲得一個hash值,並無獲得下標值哦。
重點要看這個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;
}
複製代碼
咋樣,是否是感受代碼一下變多了,咱們這裏逐步的有重點的來看,先看這個:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
複製代碼
這個table是啥?
transient Node<K,V>[] table;
複製代碼
看到了,這就是HashMap底層的那個數組,以前說了jdk1.8中數組中的每一個元素叫作Node,因此這就是個Node數組。
那麼上面那段代碼啥意思嘞?其實就是咱們第一次往HashMap中添加數據的時候,這個Node數組確定是null,還沒建立嘞,因此這裏會去執行resize這個方法。
resize方法的主要做用就是初始化和增長表的大小,說白了就是第一次給你初始化一個Node數組,其餘須要擴容的時候給你擴容
看看源碼:
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;
}
複製代碼
感受代碼也是比較多的啊,一樣,咱們關注重點代碼:
newCap = DEFAULT_INITIAL_CAPACITY;
複製代碼
有這麼一個賦值操做,DEFAULT_INITIAL_CAPACITY字面意思理解就是初始化容量啊,是多少呢?
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
複製代碼
這裏是個移位運算,就是16,如今已經肯定具體的默認容量是16了,那具體在哪建立默認的Node數組呢?繼續往下看源碼,有這麼一句
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
複製代碼
ok,到這裏咱們發現,第一次使用HashMap添加數據的時候底層會建立一個長度爲16的默認Node數組。
那麼新的問題來了?
這個問題想必你在HashMap相關分析文章中也看到過,那麼該怎麼回答呢?
想搞明白爲啥是16不是其餘的,那首先要知道爲啥HashMap的容量要是2的整數次冪?
先看這個16是怎麼來的:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
複製代碼
這裏使用了位運算,爲啥不直接16嘞?這裏主要是位運算的性能好,爲啥位運算性能就好,那是由於位運算人家直接操做內存,不須要進行進制轉換,要知道計算機但是以二進制的形式作數據存儲啊,知道了吧,那16嘞?爲啥是16不是其餘的?想要知道爲啥是16,咱們得從HashMap的數據存放特性來講。
對於HashMap而言,存放的是鍵值對,因此作數據添加操做的時候會根據你傳入的key值作hash運算,從而獲得一個下標值,也就是以這個下標值來肯定你的這個value值應該存放在底層Node數組的哪一個位置。
那麼這裏必定會出現的問題就是,不一樣的key會被計算得出同一個位置,那麼這樣就衝突啦,位置已經被佔了,那麼怎麼辦嘞?
首先就是衝突了,咱們要想辦法看看後來的數據應該放在哪裏,就是給它找個新位置,這是常規方法,除此以外,咱們是否是也能夠聚焦到hash算法這塊,就是儘可能減小衝突,讓獲得的下標值可以均勻分佈。
好了,以上巴拉巴拉說一些理念,下面咱們看看源碼中是怎麼計算下標值得:
i = (n - 1) & hash
複製代碼
這是在源碼中第629行有這麼一段,它就是計算咱們上面說的下標值的,這裏的n就是數組長度,默認的就是16,這個hash就是這裏獲得的值:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
繼續看它:
i = (n - 1) & hash
複製代碼
這裏是作位與運算,接着咱們還須要先搞明白一個問題
要知道,咱們最終是根據key經過哈希算法獲得下標值,這個是怎麼獲得的呢?一般作法就是拿到key的hashcode而後與數組的容量作取模運算,爲啥要作取模運算呢?
好比這裏默認是一個長度爲16的Node數組,咱們如今要根據傳進來的key計算一個下標值出來而後把value放入到正確的位置,想一下,咱們用key的hashcode與數組長度作取模運算,獲得的下標值是否是必定在數組的長度範圍以內,也就是獲得的下標值不會出現越界的狀況。
要知道取模是怎麼回事啊!明白了這點,咱們再來看:
i = (n - 1) & hash
複製代碼
這裏就是計算下標的,爲啥不是取模運算而是位與運算呢?使用位與運算的一方面緣由就是它的性能比較好,另一點就是這裏有這麼一個等式:
(n - 1) & hash = n % hash
複製代碼
所以,總結起來就是使用位與運算能夠實現和取模運算相同的效果,並且位與運算性能更高!
接着,咱們再看一個問題
理解了這個問題,咱們就快接近爲何容量是2的整數次冪的答案了,根據上面說的,這裏的n-1是爲了實現與取模運算相同的效果,除此以外還有很重要的緣由在裏面。
在此以前,咱們須要看看什麼是位與運算,由於我怕這塊知識你們以前不注意忘掉了,而它對理解咱們如今所講的問題很重要,看例子:
好比拿5和3作位與運算,也就是5 & 3 = 1(操做的是二進制),怎麼來的呢?
5轉換爲二進制:0000 0000 0000 0000 0000 0000 0000 0101
3轉換爲二進制:0000 0000 0000 0000 0000 0000 0000 0011
1轉換爲二進制:0000 0000 0000 0000 0000 0000 0000 0001
因此啊,位與運算的操做就是:第一個操做數的的第n位於第二個操做數的第n位若是都是1,那麼結果的第n位也爲1,不然爲0
看懂了吧,不懂得話能夠去補補這塊的知識,後續我也會單獨發文詳細說說這塊。
咱們繼續回到以前的問題,爲何作減一操做以及容量爲啥是2的整數次冪,爲啥嘞?
告訴你個祕密,2的整數次冪減一獲得的數很是特殊,有啥特殊嘞,就是2的整數次冪獲得的結果的二進制,若是某位上是1的話,那麼2的整數次冪減一的結果的二進制,以前爲1的後面全是1
啥意思嘞,可能有點繞,咱們先看2的整數次冪啊,有2,4,8,16,32等等,咱們來看,首先是16的二進制是:10000,接着16減一得15,15的二進制是:1111,再形象一點就是:
16轉換爲二進制:0000 0000 0000 0000 0000 0000 0001 0000
15轉換爲二進制:0000 0000 0000 0000 0000 0000 0000 1111
再對照我給你說的祕密,看看懂了不,能夠再來個例子:
32轉換爲二進制:0000 0000 0000 0000 0000 0000 0010 0000
31轉換爲二進制:0000 0000 0000 0000 0000 0000 0001 1111
這會總該懂了吧,而後咱們再看計算下標的公式:
(n - 1) & hash = n % hash
複製代碼
n是容量,它是2的整數次冪,而後與獲得的hash值作位於運算,由於n是2的整數次冪,減一以後的二進制最後幾位都是1,再根據位與運算的特性,與hash位與以後,獲得的結果是否是多是0也多是1,,也就是說最終的結果取決於hash的值,如此一來,只要輸入的hashcode值自己是均勻分佈的,那麼hash算法獲得的結果就是均勻的。
啥意思?這樣獲得的下標值就是均勻分佈的啊,那衝突的概率就減小啦。
而若是容量不是2的整數次冪的話,就沒有上述說的那個特性,這樣衝突的機率就會增大。
因此,明白了爲啥容量是2的整數次冪了吧。
那爲啥是16嘞?難道不是2的整數次冪都行嘛?理論上是都行,可是若是是2,4或者8會不會有點小,添加不了多少數據就會擴容,也就是會頻繁擴容,這樣豈不是影響性能,那爲啥不是32或者更大,那不就浪費空間了嘛,因此啊,16就做爲一個很是合適的經驗值保留了下來!
咱們上面也提到了,在添加數據的時候儘管爲實現下標值的均勻分佈作了不少努力,可是勢必仍是會存在衝突的狀況,那麼該怎麼解決衝突呢?
這就牽涉到哈希衝突的解決辦法了,詳情建議閱讀:來吧!一文完全搞定哈希表!
瞭解了哈希衝突的解決辦法以後咱們還要關注一個問題,那就是新的節點在插入到鏈表的時候,是怎麼插入的?
如今你應該知道,當出現hash衝突,可使用鏈表來解決,那麼這裏就有問題,新來的Node是應該放在以前Node的前面仍是後面呢?
Java8以前是頭插法,啥意思嘞,就是放在以前Node的前面,爲啥要這樣,這是以前開發者以爲後面插入的數據會先用到,由於要使用這些Node是要遍歷這個鏈表,在前面的遍歷的會更快。
可是在Java8及以後都使用尾插法了,就是放到後面,爲啥這樣?
這裏主要是一個鏈表成環的問題,啥意思嘞,想一下,使用頭插法是否是會改變鏈表的順序,你後來的就應該在後面嘛,若是擴容的話,因爲本來鏈表順序有所改變,擴容以後從新hash,可能致使的狀況就是擴容轉移後先後鏈表順序倒置,在轉移過程當中修改了原來鏈表中節點的引用關係。
這樣的話在多線程操做下就會出現死循環,而使用尾插法,在相同的前提下就不會出現這樣的問題,由於擴容先後鏈表順序是不變的,他們之間的引用關係也是不變的。
下面咱們繼續說HashMap的擴容,通過上面的分析,咱們知道第一次使用HashMap是建立一個默認長度爲16的底層Node數組,若是滿了怎麼辦,那就須要進行擴容了,也就是以前談及的resize方法,這個方法主要就是初始化和增長表的大小,關於擴容要知道這兩個概念:
這裏怎麼擴容的呢?首先是達到一個條件以後會發生擴容,什麼條件呢?就是這個負載因子,好比HashMap的容量是100,負載因子是0.75,乘以100就是75,因此當你增長第76個的時候就須要擴容了,那擴容又是怎麼樣步驟呢?
首先是建立一個新的數組,容量是原來的二倍,爲啥是2倍,想想爲啥容量是2的整數次冪,這裏擴容爲原來的2倍不正好符號這個規則嘛。
而後會通過從新hash,把原來的數據放到新的數組上,至於爲啥要從新hash,那必須啊,你容量變了,相應的hash算法規則也就變了,獲得的結果天然不同了。
在Java8以前是沒有紅黑樹的實現的,在jdk1.8中加入了紅黑樹,就是當鏈表長度爲8時會將鏈表轉換爲紅黑樹,爲6時又會轉換成鏈表,這樣時提升了性能,也能夠防止哈希碰撞攻擊,這些知識在來吧!一文完全搞定哈希表!都有詳細講解,強烈推薦閱讀
下面咱們分析一下HashMap增長新元素的時候都會作哪些步驟:
因此啊,這裏面的重點就是判斷放入HashMap中的元素要不要替換當前節點的元素,那怎麼判斷呢?總結起來只要知足如下兩點便可替換:
一、hash值相等。
二、==或equals的結果爲true。
好了,到了這裏就差很少了,開篇就說過HashMap能夠說是Java集合的精髓了,想要完全搞懂真心不容易,可是咱們所掌握的應該足夠應對日常的面試,關於HashMap更多的高級內容,後續會繼續分享。
感謝你們的閱讀,若有錯誤之處歡迎指正!
想要閱讀更多精彩內容,能夠關注個人微信公衆號:編碼以外,這是個人私人公衆號,專一於Java原創,主要涉及數據結構與算法,計算機基礎以及Java核心知識的講解,期待你的參與。