HashMap

1、爲什麼用HashMap?

  • HashMap 是一個散列桶(數組和鏈表),它存儲的內容是鍵值對 key-value 映射
  • HashMap 採用了數組和鏈表的數據結構,能在查詢和修改方便繼承了數組的線性查找和鏈表的尋址修改
  • HashMap 是非 synchronized,所以 HashMap 很快
  • HashMap 可以接受 null 鍵和值,而 Hashtable 則不能(原因就是 equlas() 方法需要對象,因爲 HashMap是後出的 API 經過處理纔可以)

2、HashMap 的工作原理是什麼?

HashMap 是基於 hashing 的原理

我們使用 put(key, value) 存儲對象到 HashMap 中,使用 get(key) 從 HashMap 中獲取對象。當我們給 put() 方法傳遞鍵和值時,我們先對鍵調用 hashCode() 方法,計算並返回的 hashCode 是用於找到 Map 數組的 bucket 位置來儲存 Node 對象。

這裏關鍵點在於指出,HashMap 是在 bucket 中儲存鍵對象和值對象,作爲Map.Node 。
以下是 HashMap 初始化

簡化的模擬數據結構:
在這裏插入圖片描述

Node[] table = new Node[16]; // 散列桶初始化,table
class Node {
    hash; //hash值
    key; //鍵
    value; //值
    node next; //用於指向鏈表的下一層(產生衝突,用拉鍊法)
}

以下是具體的 put 過程(JDK1.8)

  1. 對 Key 求 Hash 值,然後再計算下標
  2. 如果沒有碰撞,直接放入桶中(碰撞的意思是計算得到的 Hash 值相同,需要放到同一個 bucket 中)
  3. 如果碰撞了,以鏈表的方式鏈接到後面
  4. 如果鏈表長度超過閥值(TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表
  5. 如果節點已經存在就替換舊值
  6. 如果桶滿了(容量16 * 加載因子0.75),就需要 resize(擴容2倍後重排)

以下是具體 get 過程

考慮特殊情況:如果兩個鍵的 hashcode 相同,你如何獲取值對象?

當我們調用 get() 方法,HashMap 會使用鍵對象的 hashcode 找到 bucket 位置,找到 bucket 位置之後,會調用 keys.equals() 方法去找到鏈表中正確的節點,最終找到要找的值對象。
在這裏插入圖片描述
3、有什麼方法可以減少碰撞?

擾動函數可以減少碰撞

原理是如果兩個不相等的對象返回不同的 hashcode 的話,那麼碰撞的機率就會小些。這就意味着存鏈表結構減小,這樣取值的話就不會頻繁調用 equal 方法,從而提高 HashMap 的性能(擾動即 Hash 方法內部的算法實現,目的是讓不同對象返回不同 hashcode)。

使用不可變的、聲明作 final 對象,並且採用合適的 equals() 和 hashCode() 方法,將會減少碰撞的發生

不可變性使得能夠緩存不同鍵的 hashcode,這將提高整個獲取對象的速度,使用 String、Integer 這樣的 wrapper 類作爲鍵是非常好的選擇。

爲什麼 String、Integer 這樣的 wrapper 類適合作爲鍵?

因爲 String 是 final,而且已經重寫了 equals() 和 hashCode() 方法了。不可變性是必要的,因爲爲了要計算 hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的 hashcode 的話,那麼就不能從 HashMap 中找到你想要的對象。

4、HashMap 中 hash 函數怎麼是實現的?

我們可以看到,在 hashmap 中要找到某個元素,需要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。

前面說過,hashmap 的數據結構是數組和鏈表的結合,所以我們當然希望這個 hashmap 裏面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個。那麼當我們用 hash 算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。 所以,我們首先想到的就是把 hashcode 對數組長度取模運算。這樣一來,元素的分佈相對來說是比較均勻的。

但是“模”運算的消耗還是比較大的,能不能找一種更快速、消耗更小的方式?我們來看看 JDK1.8 源碼是怎麼做的(被樓主修飾了一下)

static final int hash(Object key) {
    if (key == null){
        return 0;
    }
    int h;
    h = key.hashCode();返回散列值也就是hashcode
    // ^ :按位異或
    // >>>:無符號右移,忽略符號位,空位都以0補齊
    //其中n是數組的長度,即Map的數組部分初始化長度
    return (n-1)&(h ^ (h >>> 16));
}

在這裏插入圖片描述
簡單來說就是:

  • 高16 bit 不變,低16 bit 和高16 bit 做了一個異或(得到的 hashcode 轉化爲32位二進制,前16位和後16位低16 bit 和高16 bit 做了一個異或)
  • (n·1) & hash = -> 得到下標

5、拉鍊法導致的鏈表過深,爲什麼不用二叉查找樹代替而選擇紅黑樹?爲什麼不一直使用紅黑樹?
之所以選擇紅黑樹是爲了解決二叉查找樹的缺陷:二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成層次很深的問題),遍歷查找會非常慢。而紅黑樹在插入新數據後可能需要通過左旋、右旋、變色這些操作來保持平衡。引入紅黑樹就是爲了查找數據快,解決鏈表查詢深度的問題。我們知道紅黑樹屬於平衡二叉樹,爲了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性鏈表要少。所以當長度大於8的時候,會使用紅黑樹;如果鏈表長度很短的話,根本不需要引入紅黑樹,引入反而會慢。

6、說說你對紅黑樹的見解?
在這裏插入圖片描述

  1. 每個節點非紅即黑
  2. 根節點總是黑色的
  3. 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定)
  4. 每個葉子節點都是黑色的空節點(NIL節點)
  5. 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

詳細內容請參考原文:https://mp.weixin.qq.com/s/__ZnkPAF6ucUqN8CVSVQeA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章