美團面試題:Hashmap的結構,1.7和1.8有哪些區別(史上最深入的分析)

程式設計師追風發表於2020-03-20

作者|依本多情

原文:blog.csdn.net/qq_36520235/article/details/82417949

一、真實面試題之:Hashmap的結構,1.7和1.8有哪些區別

不同點:

(1)JDK1.7用的是頭插法,而JDK1.8及之後使用的都是尾插法,那麼他們為什麼要這樣做呢?因為JDK1.7是用單連結串列進行的縱向延伸,當採用頭插法時會容易出現逆序且環形連結串列死迴圈問題。但是在JDK1.8之後是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且連結串列死迴圈的問題。

(2)擴容後資料儲存位置的計算方式也不一樣:

  1. 在JDK1.7的時候是直接用hash值和需要擴容的二進位制數進行&(這裡就是為什麼擴容的時候為啥一定必須是2的多少次冪的原因所在,因為如果只有2的n次冪的情況時最後一位二進位制數才一定是1,這樣能最大程度減少hash碰撞)(hash值 & length-1)。

  2. 而在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核心知識點總結!


相關文章