【Java入門提高篇】Day23 Java容器類詳解(六)HashMap源碼分析(中)


  上一篇中對HashMap中的基本內容做了詳細的介紹,解析了其中的get和put方法,想必大家對於HashMap也有了更好的認識,本篇將從了算法的角度,來分析HashMap中的那些函數。

HashCode

  先來說說HashMap中HashCode的算法,在上一篇里,我們看到了HashMap中的put方法是這樣的:

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

  那這個hash函數又是什么呢?讓我們來看看它的真面目:

    /**
     * 將高位與低位進行與運算來計算哈希值。因為在hashmap中使用2的整數冪來作為掩碼,所以只在當前掩碼之上的位上發生
     * 變化的散列總是會發生沖突。(在已知的例子中,Float鍵的集合在小表中保持連續的整數)因此,我們應用一個位運算
     * 來向下轉移高位的影響。 這是在綜合考慮了運算速度,效用和質量之后的權衡。因為許多常見的散列集合已經合理分布
     * (所以不能從擴散中受益),並且因為我們使用樹來處理bin中發生的大量碰撞的情況,所以我們盡可能以代價最低的方式
     * 對一些位移進行異或運算以減少系統損失, 以及合並由於hashmap容量邊界而不會被用於散列運算的最高位的影響。
     *
     * todo 擾動函數
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  可以看出,這里並不是簡單的使用了key的hashCode,而是將它的高16位與低16位做了一個異或操作。(“>>>”是無符號右移的意思,即右移的時候左邊空出的部分用0填充)這是一個擾動函數,具體效果后面會說明。接下來再看看之前的putval方法:

 1     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         //如果當前table未初始化,則先重新調整大小至初始容量
 5         if ((tab = table) == null || (n = tab.length) == 0)
 6             n = (tab = resize()).length;
 7         //(n-1)& hash 這個地方即根據hash求序號,想了解更多散列相關內容可以查看下一篇
 8         if ((p = tab[i = (n - 1) & hash]) == null)
 9             //不存在,則新建節點
10             tab[i] = newNode(hash, key, value, null);
11         else {
12             Node<K,V> e; K k;
13             //先找到對應的node
14             if (p.hash == hash &&
15                     ((k = p.key) == key || (key != null && key.equals(k))))
16                 e = p;
17             else if (p instanceof TreeNode)
18                 //如果是樹節點,則調用相應的putVal方法,這部分放在第三篇內容里
19                 //todo putTreeVal
20                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
21             else {
22                 //如果是鏈表則之間遍歷查找
23                 for (int binCount = 0; ; ++binCount) {
24                     if ((e = p.next) == null) {
25                         //如果沒有找到則在該鏈表新建一個節點掛在最后
26                         p.next = newNode(hash, key, value, null);
27                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
28                             //如果鏈表長度達到樹化的最大長度,則進行樹化,該函數內容也放在第三篇
29                             //todo treeifyBin
30                             treeifyBin(tab, hash);
31                         break;
32                     }
33                     if (e.hash == hash &&
34                             ((k = e.key) == key || (key != null && key.equals(k))))
35                         break;
36                     p = e;
37                 }
38             }
39             //如果已存在該key的映射,則將值進行替換
40             if (e != null) { // existing mapping for key
41                 V oldValue = e.value;
42                 if (!onlyIfAbsent || oldValue == null)
43                     e.value = value;
44                 afterNodeAccess(e);
45                 return oldValue;
46             }
47         }
48         //修改次數加一
49         ++modCount;
50         if (++size > threshold)
51             resize();
52         afterNodeInsertion(evict);
53         return null;
54     }

  注意看第八行的代碼:

  tab[i = (n - 1) & hash]

  (n - 1) & hash 即通過key的hash值來取對應的數組下標,並非是對table的size進行取余操作。

  那么,為什么要這樣做呢?首先,擾動函數的目的就是為了擴大高位的影響,使得計算出來的數值包含了高 16 位和第 16 位的特性,讓 hash 值更加深不可測來降低碰撞的概率。從hash方法的注釋中,我們也可以找到答案,一般的散列,其實都是做取余處理,但是HashMap中的table大小是2的整數次冪,也就是說,肯定不是質數,那么在取余的時候,偶數的映射范圍勢必就要小了一半,這樣效果顯然就差很多,而且,除法和取余其實是很慢的操作,所以在JDK8中,使用了一種很巧妙的方式來進行散列。首先,table的大小size設置成了2的整數次冪,這樣使用size-1就變成了掩碼,下面是我找的一張圖,能很好的解釋這個過程:

  n是table的大小,默認是16,二進制即為10000,n - 1 對應的二進制則為1111,這樣再與hash值做“與”操作時,就變成了掩碼,除了最后四位全部被置為0,而最后四位的范圍肯定會落在(0~n-1)之間,正好是數組的大小范圍,散列函數的妙處就在於此了。簡直不能更穩,一波操作猛如虎。

  那么我們繼續上一篇的栗子,我們來一步一步分析一下,小明和小李的hash值的映射過程:

  小明的hash值是756692,轉換為二進制為10111000101111010100,table的大小是32,n-1=31,對應的二進制為:11111,做“與”運算之后,得到的結果是10100,即為20。

  小李的hash值是757012,轉換為二進制為10111000110100010100,與11111做與運算后,得到的結果也是10100,即20,於是就與小明發生了沖突,但還是要先來后到,於是小李就掛在了小明后面。

  散列函數看完了,我們接下來再看看擴容函數。

擴容函數

  擴容函數其實之前也已經見過了,就在上面的putVal方法里,往上面翻一翻,第六行可以看到resize函數,這就是擴容函數,讓我們來看看它的廬山真面目:

  

 1     /**
 2      * 初始化或將table的大小進行擴容。 如果table為null,則按照字段threshold中的初始容量目標進行分配。
 3      * 否則,因為我們使用2次冪進行擴容,所以在新表中,來自每個bin中的元素必須保持在相同的索引處,或者以原偏移量的2次冪進行移動。
 4      */
 5     final Node<K,V>[] resize() {
 6         Node<K,V>[] oldTab = table;
 7         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 8         int oldThr = threshold;
 9         int newCap, newThr = 0;
10         if (oldCap > 0) {
11             if (oldCap >= MAXIMUM_CAPACITY) {
12                 threshold = Integer.MAX_VALUE;
13                 return oldTab;
14             }
15             //新的容量擴展成原來的兩倍
16             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
17                     oldCap >= DEFAULT_INITIAL_CAPACITY)
18                 //閾值也調整為原來的兩倍
19                 newThr = oldThr << 1; // double threshold
20         }
21         else if (oldThr > 0) // initial capacity was placed in threshold
22             newCap = oldThr;
23         else {               // zero initial threshold signifies using defaults
24             newCap = DEFAULT_INITIAL_CAPACITY;
25             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
26         }
27         if (newThr == 0) {
28             float ft = (float)newCap * loadFactor;
29             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
30                     (int)ft : Integer.MAX_VALUE);
31         }
32         threshold = newThr;
33         @SuppressWarnings({"rawtypes","unchecked"})
34         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
35         table = newTab;
36         //將舊數組中的node重新散列到新數組中
37         if (oldTab != null) {
38             for (int j = 0; j < oldCap; ++j) {
39                 Node<K,V> e;
40                 if ((e = oldTab[j]) != null) {
41                     oldTab[j] = null;
42                     if (e.next == null)
43                         newTab[e.hash & (newCap - 1)] = e;
44                     else if (e instanceof TreeNode)
45                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
46                     else { // preserve order
47                         Node<K,V> loHead = null, loTail = null;
48                         Node<K,V> hiHead = null, hiTail = null;
49                         Node<K,V> next;
50                         do {
51                             next = e.next;
52                             if ((e.hash & oldCap) == 0) {
53                                 if (loTail == null)
54                                     loHead = e;
55                                 else
56                                     loTail.next = e;
57                                 loTail = e;
58                             }
59                             else {
60                                 if (hiTail == null)
61                                     hiHead = e;
62                                 else
63                                     hiTail.next = e;
64                                 hiTail = e;
65                             }
66                         } while ((e = next) != null);
67                         if (loTail != null) {
68                             loTail.next = null;
69                             newTab[j] = loHead;
70                         }
71                         if (hiTail != null) {
72                             hiTail.next = null;
73                             newTab[j + oldCap] = hiHead;
74                         }
75                     }
76                 }
77             }
78         }
79         return newTab;
80     }

  這里可以看到,如果原來的table還未被初始化的話,調用該函數后就會被擴容到默認大小(16),上一篇中也已經說過,HashMap也是使用了懶加載的方式,在構造函數中並沒有初始化table,而是在延遲到了第一次插入元素之后。

  當使用put插入元素的時候,如果發現目前的bins占用程度已經超過了Load Factor所設置的比例,那么就會發生resize,簡單來說就是把原來的容量和閾值都調整為原來的2倍,之后重新計算index,把節點再放到新的bin中。因為index值的計算與table數組的大小有關,所以擴容后,元素的位置有可能會調整:

  以上圖為例,如果對應的hash值第五位是0,那么做與操作后,得到的序號不會變,那么它的位置就不會改變,相反,如果是1,那么它的新序號就會變成原來的序號+16,。

  

  好像也不是很多嘛,嗯,算法部分就先介紹到這里了,之后的一篇再來說說HashMap中的EntrySet,KeySet和values(如果時間夠的話順便把迭代器也說一說)。

  好了,本篇就此愉快的結束了,最后祝大家端午節快樂!如果覺得內容還不錯的話記得動動小手點關注哦,你的支持就是我最大的動力!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM