【JDK】:HashMap詳解

Hash散列基本思想

哈希表使用數組和鏈表共同實現散列存儲,每一個數組元素可以認爲是散列表中的桶位(buket),每個桶位存放一個鏈表,該鏈表由散列碼(hashCode)相同的節點構成。Hash表的查找就是根據需要查找的對象(key, value)中的key,利用散列函數計算key對應的hashCode,即數組的下標(buket的索引),在O(1)時間內找到對應的桶位,再遍歷該桶位內的鏈表,查找對應的value值即可。

在JDK8中,當桶位數目過多(默認至少64)或者某一個桶位的鏈表長度過長時(默認是8),查找效率會顯著降低。因此HashMap會將鏈表的普通節點轉化爲樹節點(TreeNode)存儲,鏈表List也將轉爲Tree樹將搜索效率提升到O(logn),但是TreeNode的空間消耗是普通Node空間消耗的兩倍,在HashMap進行多次remove操作之後,如果桶位數目和鏈表長度低於閾值,TreeNode重新轉化爲Node,Tree樹轉爲List鏈表。

Hash表長度爲2n 與查找效率

Hash表中的table數組存放node,table的長度size必須爲2的冪,在這個前提下有如下規律:對任意一個哈希碼hashCode利用求餘運算進行散列,即index=hashCode%size時,index爲hashCode所在的數組桶位下標,由於求餘取模運算效率低下,在size爲2的冪的前提下,可以用位與運算代替,即index=hashCode & (size – 1),得到的是相同的結果。(參見博文當length爲2^n, m & (length-1) 相當於 m % length 的證明

下面是保證輸入一個表的初始長度,是table的size總是2的冪:

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

重點函數實現

初始容量和負載因子

在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor):

  • Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
  • Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

簡單的說,Capacity就是bucket的大小,Load factor就是bucket填滿程度的最大比例。如果對迭代性能要求很高的話不要把capacity設置過大,也不要把load factor設置過小。當bucket中的entries的數目大於capacity*load factor時就需要調整bucket的大小爲當前的2倍。

put函數的實現

  1. 對key的hashCode()做hash,然後再計算index;
  2. 如果沒碰撞直接放到bucket裏;
  3. 如果碰撞了,以鏈表的形式存在buckets後;
  4. 如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
  5. 如果節點已經存在就替換old value(保證key的唯一性;
  6. 如果bucket滿了(超過load factor*current capacity),就要resize。
public V put(K key, V value) {
    // 對key的hashCode()做hash
    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,並對null做處理
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 節點存在
        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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                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;
    // 超過load factor*current capacity,resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

get函數實現

  1. bucket裏的第一個節點,直接命中;
  2. 如果有衝突,則通過key.equals(k)去查找對應的entry
    • 若爲樹,則在樹中通過key.equals(k)查找,O(logn);
    • 若爲鏈表,則在鏈表中通過key.equals(k)查找,O(n)。
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 直接命中
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 未命中
        if ((e = first.next) != null) {
            // 在樹中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在鏈表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

hash()、resize()

強烈推薦閱讀Java HashMap工作原理及實現,裏面有詳細的介紹。

相關問題

  1. 什麼時候會使用HashMap?他有什麼特點?
    是基於Map接口的實現,存儲鍵值對時,它可以接收null的鍵值,是非同步的,HashMap存儲着Entry(hash, key, value, next)對象。

  2. 你知道HashMap的工作原理嗎?
    通過hash的方法,通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調用hashCode計算hash從而得到bucket位置,進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,我們將K傳給get,它調用hashCode計算hash從而得到bucket位置,並進一步調用equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提高速度。

  3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?
    通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點

  4. 你知道hash的實現嗎?爲什麼要這樣實現?
    在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。

  5. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
    如果超過了負載因子(默認0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新調用hash方法。

推薦閱讀

  1. Java HashMap工作原理及實現
  2. Java 8:HashMap的性能提升
  3. HashMap多線程併發問題分析
  4. 優化哈希策略
  5. 淺析HashMap與ConcurrentHashMap的線程安全性
發佈了117 篇原創文章 · 獲贊 184 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章