1. 程式人生 > >【Java】HashMap源碼分析——常用方法詳解

【Java】HashMap源碼分析——常用方法詳解

fir 設置 直接 dfa 構造方法 change mage null 這也

上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:
put()
get()
**resize()**

首先介紹resize()這個方法,在我看來這是HashMap中一個非常重要的方法,是用來調整HashMap中table的容量的,在很多操作中多需要重新計算容量。
源碼如下:

 1 final Node<K,V>[] resize() {
 2         Node<K,V>[] oldTab = table;
 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4         int
oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) { 7 if (oldCap >= MAXIMUM_CAPACITY) { 8 threshold = Integer.MAX_VALUE; 9 return oldTab; 10 } 11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 12
oldCap >= DEFAULT_INITIAL_CAPACITY) 13 newThr = oldThr << 1; // double threshold 14 } 15 else if (oldThr > 0) // initial capacity was placed in threshold 16 newCap = oldThr; 17 else { // zero initial threshold signifies using defaults
18 newCap = DEFAULT_INITIAL_CAPACITY; 19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 20 } 21 if (newThr == 0) { 22 float ft = (float)newCap * loadFactor; 23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 24 (int)ft : Integer.MAX_VALUE); 25 } 26 threshold = newThr; 27 @SuppressWarnings({"rawtypes","unchecked"}) 28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 29 table = newTab; 30 if (oldTab != null) { 31 for (int j = 0; j < oldCap; ++j) { 32 Node<K,V> e; 33 if ((e = oldTab[j]) != null) { 34 oldTab[j] = null; 35 if (e.next == null) 36 newTab[e.hash & (newCap - 1)] = e; 37 else if (e instanceof TreeNode) 38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 39 else { // preserve order 40 Node<K,V> loHead = null, loTail = null; 41 Node<K,V> hiHead = null, hiTail = null; 42 Node<K,V> next; 43 do { 44 next = e.next; 45 if ((e.hash & oldCap) == 0) { 46 if (loTail == null) 47 loHead = e; 48 else 49 loTail.next = e; 50 loTail = e; 51 } 52 else { 53 if (hiTail == null) 54 hiHead = e; 55 else 56 hiTail.next = e; 57 hiTail = e; 58 } 59 } while ((e = next) != null); 60 if (loTail != null) { 61 loTail.next = null; 62 newTab[j] = loHead; 63 } 64 if (hiTail != null) { 65 hiTail.next = null; 66 newTab[j + oldCap] = hiHead; 67 } 68 } 69 } 70 } 71 } 72 return newTab; 73 }

可以看到這段代碼非常龐大,其內容可以分為兩大部分:
第一部分計算並生成新的哈希表(空表):

 1 // 記錄原表
 2 Node<K,V>[] oldTab = table;
 3 // 得到原來哈希表的總長度,及原來總容量
 4 int oldCap = (oldTab == null) ? 0 : oldTab.length;
 5 // 得到原來最佳容量
 6 int oldThr = threshold;
 7 // 存放新的總容量、新最佳容量的變量
 8 int newCap, newThr = 0;
 9 if (oldCap > 0) {
10 // 原來總容量達到或超過HashMap的最大容量,則最佳容量設置為int類型的最大值
11 // 且原來容量不變,直接返回,不做後需調整
12    if (oldCap >= MAXIMUM_CAPACITY) {
13        threshold = Integer.MAX_VALUE;
14        return oldTab;
15    }
16    // 讓新的總容量等於原來容量的二倍
17    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
18             oldCap >= DEFAULT_INITIAL_CAPACITY)
19        // 新的最佳容量也變為原來的二倍
20        newThr = oldThr << 1; 
21 }
22 // 原來總容量為0,將新的總容量設置為最佳容量,構造方法出入參數是一個派生的Map的時候,就會使用派生的Map計算出新的最佳容量
23 else if (oldThr > 0) 
24    newCap = oldThr;
25 else { 
26 // 原來總容量和原來最佳容量都沒有定義
27 // 新的總容量設為默認值16
28 // 新的最佳容量=默認負載因子×默認容量=0.75×16=12              
29    newCap = DEFAULT_INITIAL_CAPACITY;
30    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
31 }
32 // 判斷上述操作後新的最佳容量是否計算,若沒有,就利用負載因子和新的總容量計算
33 if (newThr == 0) {
34    float ft = (float)newCap * loadFactor;
35    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
36              (int)ft : Integer.MAX_VALUE);
37 }
38 // 更新當前的最佳容量
39 threshold = newThr;
40 @SuppressWarnings({"rawtypes","unchecked"})
41 // 生成新的哈希表,即一維數組
42 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
43 // 更新哈希表
44 table = newTab;

可以看出上述操作僅僅是生成了一張大小合適的哈希表,但表還是空的,後面的操作就是把以前的表中的元素重新排列,移動到當前表中合適的位置!

第二部分將原表元素移動到新表合適的位置:

 1 // 先判斷原表是或否為空
 2 if (oldTab != null) {
 3     // 遍歷原表(一維數組)中的所有元素,
 4    for (int j = 0; j < oldCap; ++j) {
 5            // 記錄原來一維數組中下標為j的元素
 6        Node<K,V> e;
 7        // 只對有效元素進行操作
 8        if ((e = oldTab[j]) != null) {
 9                //將原表中的元素置空
10            oldTab[j] = null;
11            if (e.next == null)
12            // 當前元素沒有後繼,那麽直接把它放在新表中合適位置
13            // 其中e.hash & (newCap - 1)在我上一篇博客有介紹
14            // 就是以該節點的hash值和新表總容量取余,將余數作為下標
15                newTab[e.hash & (newCap - 1)] = e;
16            else if (e instanceof TreeNode)
17                // 當前元素有後繼,且後繼是紅黑樹
18                // 進行有關紅黑樹的相應操作
19                // 這裏不詳細介紹紅黑樹的操作
20                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
21            else { 
22            // 這裏就進行有關鏈表的移動
23                   // 這兩組結點變量,分別代表兩條不同鏈表的頭和尾
24                   // 低位的頭和尾 
25                Node<K,V> loHead = null, loTail = null;
26                // 高位的頭和尾
27                Node<K,V> hiHead = null, hiTail = null;
28                // 下一節點
29                Node<K,V> next;
30                do {
31                       // 讓next等於當前結點的後繼結點
32                    next = e.next;
33                    // 這個位運算實際上判斷的是該節點在新表中的位置是否發生改變
34                    // 成立則說明沒有改變,還是原來表中下標為j的位置
35                    if ((e.hash & oldCap) == 0) {
36                            // 若是首結點,則讓低位的頭等於當前結點
37                        if (loTail == null)
38                            loHead = e;
39                        else
40                        // 若不是首結點,則讓低位的尾等於當前結點
41                            loTail.next = e;
42                        // 讓低位的尾移動到當前
43                        loTail = e;
44                    }
45                    // 這裏就說明其在新表中的位置發生了改變,則要將其放入另一條鏈表
46                    else {
47                           // 若是首結點,則讓高位的頭等於當前結點
48                        if (hiTail == null)
49                            hiHead = e;
50                        else
51                               // 若不是首結點,則讓高位的尾等於當前結點
52                            hiTail.next = e;
53                        // 讓高位的尾移動到當前
54                        hiTail = e;
55                    }
56                } while ((e = next) != null);
57                // 原來位置的這條鏈表還存在
58                if (loTail != null) {
59                       // 置空低位的尾的next
60                    loTail.next = null;
61                    // 將該鏈表的頭結點放入新表下標為j的位置,即原表中的原位置
62                    newTab[j] = loHead;
63                }
64                // 新位置上的鏈表存在
65                if (hiTail != null) {
66                       // 置空高位的尾的next
67                    hiTail.next = null;
68                    // 將該鏈表的頭結點放入新表中下標為j+原表長度的位置
69                    newTab[j + oldCap] = hiHead;
70                }
71            }
72        }
73    }
74 }
75 return newTab;

鏈表的移動如圖:

技術分享圖片

可以看出,這個方法可以使得單個結點重新散列,鏈表可以拆分成兩條,紅黑樹重新移動,這樣使得新的哈希表分布比以前均勻!

下面來分析put方法:
源碼如下:

1  public V put(K key, V value) {
2      return putVal(hash(key), key, value, false, true);
3  }

這裏我們可以知道其調用了內部的一個putVal方法:
首先第一個參數是通過內部的hash方法(在前一篇博客有介紹過)計算出鍵對象的hash(int類型)值,再把key和value對象傳過去,置於後面兩個參數先不著急
先來看下putVal方法是如何說明的:

 1 /**
 2      * Implements Map.put and related methods
 3      *
 4      * @param hash hash for key
 5      * @param key the key
 6      * @param value the value to put
 7      * // 看以看出,put方法傳入的onlyIfAbsent是false,那麽就會改變原來已存在的值
 8      * @param onlyIfAbsent if true, don‘t change existing value
 9      * // 這個參數先不考慮,往後慢慢分析
10      * @param evict if false, the table is in creation mode.
11      * @return previous value, or null if none
12      */
13     final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

該方法內容:

 1  // 用於保存原表
 2  Node<K,V>[] tab;
 3  // 保存下標為hash的結點 
 4  Node<K,V> p;
 5  // n用來記錄表長
 6  int n, i;
 7  // 先檢查原表是否存在,或者是空表
 8  if ((tab = table) == null || (n = tab.length) == 0)
 9       // 如果為空就生成一張大小為16的新表
10      n = (tab = resize()).length;
11  if ((p = tab[i = (n - 1) & hash]) == null)
12       // 如果以該方法形參hash對表長取余,令其作為下標的表中的元素為空,那麽就產生一個新結點放在這個位置
13      tab[i] = newNode(hash, key, value, null);
14  else {
15       // 如果該結點不空,那麽就會出現兩種情況:鏈表和紅黑樹
16      Node<K,V> e; K k;
17      if (p.hash == hash &&
18          ((k = p.key) == key || (key != null && key.equals(k))))
19          // 如果當前結點的hash並且key值(指針值和內容值)相等,由於onlyIfAbsent是false,那麽就會改變這個結點的V值,先用e將其保存起來
20          e = p;
21      else if (p instanceof TreeNode)
22          // 如果當前結點是一棵紅黑樹,那麽就進行紅黑樹的平衡,這裏不討論紅黑樹的問題
23          e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24      else {
25           // 這裏就對鏈表進行操作
26           // 從頭開始遍歷這條鏈表
27          for (int binCount = 0; ; ++binCount) {
28              if ((e = p.next) == null) {
29                   // 如果該節點的next為空
30                   // 就需要新增一個結點追加其後
31                  p.next = newNode(hash, key, value, null);
32                  if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
33                       // 這裏進行紅黑樹閾值的判斷,由於TREEIFY_THRESHOLD默認值是8,binCount是從0開始,那麽當鏈表長度大於等於8的時候,就將該鏈表轉換成紅黑樹,並且結束循環
34                      treeifyBin(tab, hash);
35                  break;
36              }
37              // 這裏和之前的判斷是一樣的
38              if (e.hash == hash &&
39                  ((k = e.key) == key || (key != null && key.equals(k))))
40                  break;
41              // 讓p = p->next
42              p = e;
43          }
44      }
45      // 若e非空,則就是說明原表中存在hash值相等,且key的值或內容相同的結點
46      if (e != null) { 
47          // 將原來的V值保存
48          V oldValue = e.value;
49          // 判斷是否是需要進行覆蓋原來V值的操作
50          if (!onlyIfAbsent || oldValue == null)
51              // 覆蓋原來的V值
52              e.value = value;
53          // 這個方法是一個空的方法,預留的一個操作,不用去管它     
54          afterNodeAccess(e);
55          // 由於在這裏面的操作只是替換了原來的V值,並沒有改變原來表的大小,直接返回oldValue
56          return oldValue;
57      }
58  }
59  // 操作數自增
60  ++modCount;
61  // 實際大小自增
62  // 若其大於最佳容量進行擴容的操作,使其分布均勻
63  if (++size > threshold)
64      resize();
65  // 這也是一個空的方法,預留操作
66  afterNodeInsertion(evict);
67  // 並沒有替換原來的V值,返回null
68  return null;


下來是get方法,邏輯相對簡單不難分析:

1 public V get(Object key) {
2     Node<K,V> e;
3     return (e = getNode(hash(key), key)) == null ? null : e.value;
4 }

同樣也是通過hash方法計算出key對象的hash值,調用內部的getNode方法:

 1 final Node<K,V> getNode(int hash, Object key) {
 2     // 記錄表對象
 3     Node<K,V>[] tab;
 4     // 記錄第一個結點和當前節點 
 5     Node<K,V> first, e; 
 6     // 記錄表長
 7     int n; 
 8     // 記錄K值
 9     K k;
10     // 表非空或者長度大於0才對其操作
11     // 並且key的hash值對表長取余為下標,其所對應的哈希表中的結點存在
12     if ((tab = table) != null && (n = tab.length) > 0 &&
13         (first = tab[(n - 1) & hash]) != null) {
14         // 當前結點滿足情況,直接返回給該節點
15         if (first.hash == hash && 
16             ((k = first.key) == key || (key != null && key.equals(k))))
17             return first;
18         // 後面就分為兩種情況:在紅黑樹或者鏈表中查找
19         if ((e = first.next) != null) {
20             // 當前結點是紅黑樹,進行紅黑樹的查找
21             if (first instanceof TreeNode)
22                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
23             // 進行鏈表的遍歷
24             do {
25                 if (e.hash == hash &&
26                     ((k = e.key) == key || (key != null && key.equals(k))))
27                     return e;
28             } while ((e = e.next) != null);
29         }
30     }
31     return null;
32 }

若有不足還請指出!

我在CSDN也放了一篇【Java】HashMap源碼分析——常用方法詳解

【Java】HashMap源碼分析——常用方法詳解