《Java集合》HashMap實現詳解

1. 實現原理

JDK1.7中的HashMap是由數組+鏈表組成的,而JDK1.8中的HashMap是由數組+鏈表+紅黑樹組成。數組的默認長度(DEFAULT_INITIAL_CAPACITY)爲16,加載因子(DEFAULT_LOAD_FACTOR)爲0.75

  • HashMap的默認長度爲16和規定數組長度爲2的冪,是爲了降低哈希碰撞的機率。
  • HashMap中使用鏈表主要爲了解決哈希衝突,鏈表出現越少或者長度越小,性能纔會越好。

數組:具有遍歷快,增刪慢的特點。數組在堆中是一塊連續的存儲空間,遍歷速度快,時間複雜度爲O(1) ;當在中間插入或刪除元素時,會造成該元素後面所有元素地址的改變,所以增刪慢,增刪的時間複雜度爲O(n)

鏈表:鏈表具有增刪快,遍歷慢的特點。鏈表中各元素的內存空間是不連續的,一個節點至少包含節點數據與後繼節點的引用,所以在插入刪除時,只需修改該位置的前驅節點與後繼節點即可,增刪時間複雜度爲O(1)。但是在遍歷時,get(n)元素時,需要從第一個開始,依次拿到後面元素的地址進行遍歷,直到遍歷到第n個元素,遍歷時間複雜度爲O(n) ,所以遍歷效率極低。

2. 哈希衝突

指兩個元素通過hash函數計算出的值是一樣的,表示這兩個元素存儲的是同一個地址。當後面的元素要插入到這個地址時,發現已經被佔用了,這時候就產生了哈希衝突。

哈希衝突的解決辦法:

  • 開放定址法:當發生哈希衝突時,查詢產生衝突的地址的下一個地址是否被佔用,直到尋找到空的地址。
  • 鏈地址法:當發生哈希衝突時,在衝突的地址上生成一個鏈表,將衝突的元素的key通過equals進行比較,相同即覆蓋,不同則添加到鏈表上。

HashMap使用的是鏈地址法。在JDK1.7中,如果鏈表過長,效率就會大大降低,查找和添加操作的時間複雜度都爲O(n);在JDK1.8中,如果鏈表長度大於8,鏈表就會轉化爲紅黑樹,時間複雜度也就將爲O(logn),性能得到了很大的提升。

當紅黑樹節點個數少於6的時候,又會將紅黑樹轉化爲鏈表。因爲在數據量較小的情況下,紅黑樹要維持平衡,比起鏈表,性能上的優勢並不明顯。

3. Rehash擴容機制

如果HashMap的大小超過了負載因子(默認爲0.75)定義的容量,也就是說,當一個Map填滿了75%的Bucket時候,將會創建原來HashMap大小的兩倍的Bucket數組,來重新調整Map的大小,並將原來的對象放入新的Bucket數組中。

閾值 = 數組默認的長度 x 負載因子(閾值 = 16 x 0.75 = 12)

1. HashMap擴容限制的負載因子爲什麼是0.75?爲什麼不能是0.1或者1呢?

  • 如果負載因子爲0.5甚至更低的可能的話,最後得到的臨時閾值明顯會很小,這樣的情況就會造成內存的浪費,存在多餘的沒用的內存空間,也不滿足哈希表均勻分佈的情況。
  • 如果負載因子達到了1的情況,也就是數組存滿了才發生擴容,這樣會出現大量的哈希衝突的情況,出現鏈表過長,因此造成get查詢數據的效率。

2. 爲何數組容量必須是2次冪?
索引計算公式爲index = (length - 1) & hash,如果length爲2次冪,那麼length-1的低位就全是1,哈希值進行與操作時可以保證低位的值不變,效果等同於hash%length,從而保證分佈均勻

JDK1.8中在擴容HashMap的時候,不需要像JDK1.7中去重新計算元素的hash,只需要看看原來的hash值新增的哪個二進制數是1還是0就好了「是0還是1可以認爲是隨機的」,如果是0的話表示索引沒有變,是1的話表示索引變成“oldCap+原索引”,這樣即省去了重新計算hash值的時間,並且擴容後鏈表元素位置不會倒置。


4. JDK1.7源碼分析

JDK1.7中的HashMap是由數組+鏈表組成的,組成鏈表結點的是Entity包含三個元素:keyvalue和指向下一個Entity的next

  • 如果定位到的數組位置不含鏈表(當前Entity的next指向null),那麼對於查找和添加等操作的執行速度很快,僅需要一次尋址,時間複雜度爲O(1)。
  • 如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n)。首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作仍需要遍歷鏈表,然後通過equals方法逐一比對查找。

4.1 put()

  1. table[index]表示通過hash值計算出元素需要存儲在數組中的位置(bucket桶),先判斷該位置上是否存在Entity。
  2. 如果不存在Entity,在該位置上插入一個Entity<k,v>對象,插入結束。
  3. 如果存在Entity,通過equals方法將key和已有的key進行比較,檢查是否相同。
    • 如果相同,新的value替換老的value;
    • 如果不相同,則在table[index]插入該Entity,並將新的Entity的next指向原來的Entity,新插入的Entity的位置永遠是在鏈表的最前面(頭插法)。

如果多線程同時put,如果同時觸發了Rehash擴容操作,會導致HashMap中的鏈表中出現循環節點,進而使得後面get的時候,會出現死循環,所以HashMap是非線程安全的

4.2 get()

先定位到數組元素,再遍歷該元素處的鏈表,在尋找目標元素的時候,除了對比通過key計算出來的hash值,還會用雙等或equals方法對key本身來進行比較,兩者都爲true時纔會返回這個元素。

  • 按照散列函數的定義,如果兩個對象相同,即obj1.equals(obj2) = true,則它們的hashCode必須相同;但如果兩個對象不同,則它們的hashCode不一定不同。
  • 如果兩個不同對象的hashcode相同,就稱爲衝突。衝突會導致操作哈希表的時間開銷增大,所以覆蓋了equals方法之後一定要覆蓋hashCode方法。比如,
String a = new String(“abc”);
String b = new String(“abc”);

如果不覆蓋的話,那麼a和b的hashCode就會不同,把這兩個類當做key存到HashMap中的話就會出現問題,就會和key的唯一性相矛盾。

如何定位元素?二進制 hashCode & (leng-1)

  1. 計算"book"的hashcode
    十進制 : 3029737
    二進制 : 101110001110101110 1001
  2. HashMap長度是默認的 16,length - 1 的結果
    十進制 : 15
    二進制 : 1111
  3. 把以上兩個結果做與運算
    101110001110101110 1001 & 1111 = 1001
    1001的十進制 : 9,所以 index=9。

5. JDK1.8源碼分析

JDK1.7中的HashMap是由數組+鏈表+紅黑樹組成的,組成鏈表結點的是Node包含三個元素:keyvalue和指向下一個Node的next

5.1 put()

  1. 判斷鍵值對數組table[i]是否爲空或爲null,如果爲空則創建Node;
  2. 根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不爲空,則轉向③;
  3. 判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals
  4. 判斷table[i]是否爲treeNode紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
  5. 遍歷table[i],判斷鏈表長度是否大於8,如果鏈表長度大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
  6. 插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行resize()擴容。
public V put(K key, V value) {
     // 對key做hash運算得到hashCode
     return putVal(hash(key), key, value, false, true);
 }

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
     // 步驟①:tab爲空則創建。
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     // 步驟②:計算index。
     if ((p = tab[i = (n - 1) & hash]) == null) 
         tab[i] = newNode(hash, key, value, null);
     else {
         Node<K,V> e; K k;
         // 步驟③:節點key存在,直接覆蓋value。
         if (p.hash == hash &&
             ((k = p.key) == key || (key != null && key.equals(k))))
             e = p;
         // 步驟④:判斷該鏈是否爲紅黑樹。
         else if (p instanceof TreeNode)
             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         // 步驟⑤:鏈表
         else {
             for (int binCount = 0; ; ++binCount) {
                 if ((e = p.next) == null) {
                     p.next = newNode(hash, key,value,null);
                      // 判斷鏈表長度是否大於8,如果鏈表長度大於8轉換爲紅黑樹進行處理。
                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                         treeifyBin(tab, hash);
                     break;
                 }
                  // 如果key已經存在並且equals相等,則直接覆蓋value。
                 if (e.hash == hash &&
                     ((k = e.key) == key || (key != null && key.equals(k)))) 
                            break;
                 p = e;
             }
         }
         if (e != null) { // existing mapping for key
             V oldValue = e.value;
             if (!onlyIfAbsent || oldValue == null)
                 e.value = value;
             afterNodeAccess(e);
             return oldValue;
         }
     }     ++modCount;
     // 步驟⑥:超過最大容量就擴容。
     if (++size > threshold)
         resize();
     afterNodeInsertion(evict);
     return null;
 }

5.2 get()

  1. 判斷表是否爲空,並且計算索引位置,並對索引位置的值進行判空校驗。
    • 如果表爲空 && 索引位置沒有值,直接返回null;
    • 如果表不爲空 && 索引位置有值,執行步驟②。
  2. 判斷入參key與索引處第一個key的hashCode是否相等、 key是否相等或者equals是否相等。
    • 如果步驟②條件滿足,則直接返回;否則執行步驟③;
  3. 判斷鏈接是否爲紅黑樹。
    • 如果鏈表是紅黑樹,則按照紅黑樹二叉查找法獲取值;
    • 如果不是紅黑樹(鏈表長度小於8爲普通鏈表),則遍歷鏈表,直到找到與入參key的hashCode相等、equals相等的key,並獲取該key的值。
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // Node不爲空 && 計算索引位置並且索引處有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 判斷key的hashCode是否相等 
            // && 判斷索引處第一個key與傳入key是否相等 
            // && 判斷key的equals是否相等。
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 判斷鏈表是否是紅黑樹,如果是紅黑樹,就從樹中獲取值。
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 如果不是紅黑樹,遍歷鏈表。
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

6. 使用示例

public class HashMapDemo {

    @Test
    public void test1() {
        // 第一種:普通使用,二次取值
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < 1000000; i++) {
            map.put("test" + i, "test" + i);
        }
        long before = System.currentTimeMillis();
        for (String key : map.keySet()) {
            map.get(key);
        }
        long after = System.currentTimeMillis();
        System.out.println("HashMap遍歷:" + (after - before) + "ms");
    }

    @Test
    public void test2() {
        // 推薦,尤其是容量大時
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < 1000000; i++) {
            map.put("test" + i, "test" + i);
        }
        long before = System.currentTimeMillis();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            entry.getValue();
            entry.getKey();
        }
        long after = System.currentTimeMillis();
        System.out.println("HashMap遍歷:" + (after - before) + "ms");
    }

    @Test
    public void test3() {
        // 通過Map.entrySet使用iterator遍歷key和value
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < 1000000; i++) {
            map.put("test" + i, "test" + i);
        }
        long before = System.currentTimeMillis();
        Iterator<Entry<String, Object>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Object> entry = (Entry<String, Object>) iterator.next();
            entry.getKey();
            entry.getValue();
        }
        long after = System.currentTimeMillis();
        System.out.println("HashMap遍歷:" + (after - before) + "ms");
    }

    @Test
    public void test4() {
        // 通過Map.values()遍歷所有的value,但不能遍歷key
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < 1000000; i++) {
            map.put("test" + i, "test" + i);
        }
        long before = System.currentTimeMillis();
        for (@SuppressWarnings("unused") Object object : map.values()) {

        }
        long after = System.currentTimeMillis();
        System.out.println("HashMap遍歷:" + (after - before) + "ms");
    }

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