1. 程式人生 > >HashMap原始碼分析之面試必備

HashMap原始碼分析之面試必備

 

    今天我們就面試會問到關於HashMap的問題進行一個彙總,以及對這些問題進行解答。

    1、HashMap的資料結構是什麼?

    2、為啥是執行緒不安全的?

    3、Hash演算法是怎樣實現的?

    4、HashMap是如何處理Hash碰撞的?

    5、增加元素的方法是怎麼實現的?

    6、獲取元素的方法時怎麼實現的?

  

 

    以上這些問題在面試中出現的頻率往往比較高,在對HashMap不太瞭解的情況下,往往很難將這些問題答全,筆者就帶領對這塊不熟悉的小夥伴們一起,一步一步解析以上的問題。

 

1、HashMap的資料結構是什麼?

    對於這個問題,筆者建議回答的時候對JAVA版本進行區分,因為不同版本下,HashMap的結構是有些差異的。

    回答:在JDK1.8之前HashMap是陣列+連結串列的形式,JDK1.8包括之後是陣列+連結串列+紅黑樹。本文講的HashMap是基於JDK1.8。

 

2、為啥是執行緒不安全的?

    多個執行緒某個時刻同時操作HashMap並執行put操作,且Hash值相同,這個時候需要解決衝突。很多方法如put() 、addEntry() 、resize() 等都不是同步的。

 

3、Hash演算法是如何實現的?

    "^"為異或符號其計算機符號為xor,相同為0,相異為1,如0^0=0 、0^1=1.">>>"為右移動符號,左側補零。圖中h>>>16就是將h的高16位換到h的低16位,而之前的高16位全補零。

    這裡有童鞋可能就會問了,為什麼要進行這個向右移16位且異或的操作?

 

4、HashMap是如何處理Hash碰撞的?

HashMap採用的是連結串列法,將hash值相同的元素放在一個連結串列下。

 

 5、增加元素的方法是怎麼實現的?

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict
 3 ) {
 4         Node<K,V>[] tab; Node<K,V> p; int n, i;
 5         // 如果說桶(也就是陣列,以下都用"桶"代替)為空,或者桶大小為0 則進行初始化
 6         // 這裡要區桶大小 和 桶內元素的大小 桶大小是指桶裝東西的能力
 7         // 桶內元素大小 是指桶裝了多少東西 
 8         if ((tab = table) == null || (n = tab.length) == 0)
 9             n = (tab = resize()).length;
10         // 這裡是幫助元素查詢元素在桶中的定位 如果定位的位置沒有元素 那麼
11         // 直接將元素放入桶的該位置就行    
12         if ((p = tab[i = (n - 1) & hash]) == null)
13             tab[i] = newNode(hash, key, value, null);
14         else {
15         // 執行到這說明定位的位置已經有元素了
16             Node<K,V> e; K k;
17         // 既然有人霸佔元素的位置,那麼就要與該元素進行對比,看看自己的Hash值和
18         // key值是不是和該位置的元素一直,如果都一直就記錄下該元素以下為e
19         // 說明有一個和我插入元素的key一樣的元素 後續可能要用新值替換舊值
20             if (p.hash == hash &&
21                 ((k = p.key) == key || (key != null && key.equals(k))))
22                 e = p;
23         // 如果只是Hash值相等而key不等,這裡就是Hash碰撞啦,要解決hash碰撞
24         // hashMap採用的是鏈地址法 就是碰撞的元素連成一個連結串列 這裡由於連結串列
25         // 如果太長就會樹化成紅黑樹,以下是判斷p也就是桶裡放的是不是紅黑樹
26             else if (p instanceof TreeNode)
27         // 是紅黑樹 我們就把節點放入紅黑樹 注意:這裡也不是一定插入到樹中,
28         // 因為如果我們要插入的元素和紅黑樹中某個節點的key相同的話,也會考慮
29                 // 新值換舊值的問題
30                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
31             else {
32         // 跳到這 說明p不是樹,而是連結串列 binCount用來記錄連結串列中元素的個數,那麼
33         // 為啥要記錄連結串列中元素的個數呢?主要判斷連結串列是否需要樹化成紅黑樹
34                 for (int binCount = 0; ; ++binCount) {
35                     // e的後一個節點為空 那麼直接掛上我們要插入的元素
36                     if ((e = p.next) == null) {
37                         p.next = newNode(hash, key, value, null);
38                         // TREEIFY_THRESHOLD 是樹化的閾值且其值為8
39                         // 這裡要注意:我們要插入的節點p是還沒有加到binCount中的
40                         // 也就是說這裡雖然binCount>=7就可以樹化,其實真正的樹化
41                         // 條件是連結串列中元素個數大於等於8
42                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
43                             treeifyBin(tab, hash);
44                         break;
45                     }
46                     // 待插入的key在連結串列中找到了,記錄下來然後退出
47                     if (e.hash == hash &&
48                         ((k = e.key) == key || (key != null && key.equals(k))))
49                         break;
50                     p = e;
51                 }
52             }
53             // 說明找到了key相同的元素
54             if (e != null) { // existing mapping for key
55                 V oldValue = e.value;
56                 // 判斷是否需要舊值換新值,預設情況下是允許更換的
57                 if (!onlyIfAbsent || oldValue == null)
58                     e.value = value;
59                 // 這個方法點進去就是個空方法,主要是為了給繼承HashMap的
60                 // LinkedHashMap服務的
61                 afterNodeAccess(e);
62                 return oldValue;
63             }
64         }
65         // 修改次數+1
66         ++modCount;
67         // 看下達到擴容的閥值沒
68         if (++size > threshold)
69         // 擴容 ,在本方法的前面需要初始化的時候也出現過
70             resize();
71          // 這個方法同樣也是為LinkedHashMap服務的
72         afterNodeInsertion(evict);
73         // 沒找到元素 就返回空
74         return null;
75     }

 

 

 6、獲取元素的方法時怎麼實現的?

    使用hash值去找桶的位置:

 1    final Node<K,V> getNode(int hash, Object key) {
 2         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
 3         // 桶不為空 並且桶的元素大於0 同時定位的位置元素還不為空 那就順藤摸瓜
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (first = tab[(n - 1) & hash]) != null) {
 6             // 第一個元素是不是我們要找的啊?判斷一下,是就返回
 7             if (first.hash == hash && // always check first node
 8                 ((k = first.key) == key || (key != null && key.equals(k))))
 9                 return first;
10             if ((e = first.next) != null) {
11             // 第一個元素不是我們要找的,而且後面還接著元素 判斷一下是不是樹
12                 if (first instanceof TreeNode)
13                     // 是樹 按照樹的獲取節點方法去獲取
14                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
15                 // 到這說明是連結串列了 那就按照連結串列的方式去迴圈
16                 do {
17                     if (e.hash == hash &&
18                         ((k = e.key) == key || (key != null && key.equals(k))))
19                         return e;
20                 } while ((e = e.next) != null);
21             }
22         }
23         return null;
24     }

7、最後插播一下HashMap中的屬性

 1 // 初始容量1向左移動4位為16
 2  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 3  // 最大容量1向左移30位
 4  static final int MAXIMUM_CAPACITY = 1 << 30;
 5  // 載入因子 也就是桶大小使用要是超過0.75 那麼就要考慮擴容了
 6  static final float DEFAULT_LOAD_FACTOR = 0.75f; 
 7  //  連結串列太長 要變樹的閥值
 8  static final int TREEIFY_THRESHOLD = 8;
 9  // 樹變成連結串列的閥值
10  static final int UNTREEIFY_THRESHOLD = 6;
11   // TREEIFY_THRESHOLD達到8也不一定樹化,還要容量達到64
12  static final int MIN_TREEIFY_CAPACITY = 64;

 下一期我們對今天講的put、get方法內涉及到的重要的方法進行講解。拜了個拜!

大量面試經驗以及學習資料書籍請關注微信公眾號:AVAJ
回覆"offer"進行獲取
365篇大廠java面經 你想要的我這裡