【Java】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方法,邏輯相對簡單不難分析:

在此我向你們推薦一個架構學習交流羣。交流學習羣號:821169538  裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。
 

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 }
相關文章
相關標籤/搜索