作者|依本多情
原文:blog.csdn.net/qq_36520235/article/details/82417949
一、真實面試題之:Hashmap的結構,1.7和1.8有哪些區別
不同點:
(1)JDK1.7用的是頭插法,而JDK1.8及之後使用的都是尾插法,那麼他們為什麼要這樣做呢?因為JDK1.7是用單連結串列進行的縱向延伸,當採用頭插法時會容易出現逆序且環形連結串列死迴圈問題。但是在JDK1.8之後是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且連結串列死迴圈的問題。
(2)擴容後資料儲存位置的計算方式也不一樣:
在JDK1.7的時候是直接用hash值和需要擴容的二進位制數進行&(這裡就是為什麼擴容的時候為啥一定必須是2的多少次冪的原因所在,因為如果只有2的n次冪的情況時最後一位二進位制數才一定是1,這樣能最大程度減少hash碰撞)(hash值 & length-1)。
而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而不再是JDK1.7的那種異或的方法。但是這種方式就相當於只需要判斷Hash值的新增參與運算的位是0還是1就直接迅速計算出了擴容後的儲存方式。
在計算hash值的時候,JDK1.7用了9次擾動處理=4次位運算+5次異或,而JDK1.8只用了2次擾動處理=1次位運算+1次異或。
擴容流程對比圖:
(3)JDK1.7的時候使用的是陣列+ 單連結串列的資料結構。但是在JDK1.8及之後時,使用的是陣列+連結串列+紅黑樹的資料結構(當連結串列的深度達到8的時候,也就是預設閾值,就會自動擴容把連結串列轉成紅黑樹的資料結構來把時間複雜度從O(n)變成O(logN)提高了效率)。
這裡再進行補充兩個問題:
(1)為什麼在JDK1.7的時候是先進行擴容後進行插入,而在JDK1.8的時候則是先插入後進行擴容的呢?
//其實就是當這個Map中實際插入的鍵值對的值的大小如果大於這個預設的閾值的時候(初始是16*0.75=12)的時候才會觸發擴容,
//這個是在JDK1.8中的先插入後擴容
if (++size > threshold)
resize();
複製程式碼
其實這個問題也是JDK8對HashMap中,主要是因為對連結串列轉為紅黑樹進行的優化,因為你插入這個節點的時候有可能是普通連結串列節點,也有可能是紅黑樹節點,但是為什麼1.8之後HashMap變為先插入後擴容的原因,我也有點不是很理解?歡迎來討論這個問題?
但是在JDK1.7中的話,是先進行擴容後進行插入的,就是當你發現你插入的桶是不是為空,如果不為空說明存在值就發生了hash衝突,那麼就必須得擴容,但是如果不發生Hash衝突的話,說明當前桶是空的(後面並沒有掛有連結串列),那就等到下一次發生Hash衝突的時候在進行擴容,但是當如果以後都沒有發生hash衝突產生,那麼就不會進行擴容了,減少了一次無用擴容,也減少了記憶體的使用。
void addEntry(int hash, K key, V value, int bucketIndex) {
//這裡當錢陣列如果大於等於12(假如)閾值的話,並且當前的陣列的Entry陣列還不能為空的時候就擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
//擴容陣列,比較耗時
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//把新加的放在原先在的前面,原先的是e,現在的是new,next指向e
table[bucketIndex] = new Entry<>(hash, key, value, e);//假設現在是new
size++;
}
複製程式碼
(2)為什麼在JDK1.8中進行對HashMap優化的時候,把連結串列轉化為紅黑樹的閾值是8,而不是7或者不是20呢(面試蘑菇街問過)?
如果選擇6和8(如果連結串列小於等於6樹還原轉為連結串列,大於等於8轉為樹),中間有個差值7可以有效防止連結串列和樹頻繁轉換。假設一下,如果設計成連結串列個數超過8則連結串列轉換成樹結構,連結串列個數小於8則樹結構轉換成連結串列,如果一個HashMap不停的插入、刪除元素,連結串列個數在8左右徘徊,就會頻繁的發生樹轉連結串列、連結串列轉樹,效率會很低。
還有一點重要的就是由於treenodes的大小大約是常規節點的兩倍,因此我們僅在容器包含足夠的節點以保證使用時才使用它們,當它們變得太小(由於移除或調整大小)時,它們會被轉換回普通的node節點,容器中節點分佈在hash桶中的頻率遵循泊松分佈,桶的長度超過8的概率非常非常小。所以作者應該是根據概率統計而選擇了8作為閥值。
//Java中解釋的原因
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
複製程式碼
二、雜湊表如何解決Hash衝突?
三、為什麼HashMap具備下述特點:鍵-值(key-value)都允許為空、執行緒不安全、不保證有序、儲存位置隨時間變化
四、為什麼 HashMap 中 String、Integer 這樣的包裝類適合作為 key 鍵
五、HashMap 中的 key若 Object型別, 則需實現哪些方法?
最後
歡迎關注公眾號:程式設計師追風,回覆66 領取一份300頁pdf文件的Java核心知識點總結!