1. 程式人生 > >關於HashMap要知道的事

關於HashMap要知道的事

HashMap資料結構

底層使用了陣列+連結串列。

如果,連結串列的長度大於等於8(TREEIFY_THRESHOLD)了,則將連結串列改為紅黑樹,這是Java8 的一個新的優化。

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
	treeifyBin(tab, hash);

當發生 雜湊衝突(碰撞)的時候,HashMap 採用 拉鍊法 進行解決。

 

擴容開銷很大(需要建立新陣列、重新雜湊、分配等等),與擴容相關的兩個因素:

預設載入因子(DEFAULT_LOAD_FACTOR)是 0.75。HashMap 使用此值基本是平衡了效能和空間的取捨。

  • 載入因子太大的話發生衝突的可能就會大,查詢的效率反而變低
  • 太小的話頻繁 rehash,導致效能降低

載入因子決定了 HashMap 中的元素佔有多少比例時擴容

初識容量(DEFAULT_INITIAL_CAPACITY)16。

 

HashMap擴容的時機:

容器中的元素數量 > 負載因此 * 容量,如果負載因子是0.75,容量是16,那麼當容器中數量達到13 的時候就會擴容。

還有,如果某個連結串列長度達到了8,並且容量小於64(MIN_TREEIFY_CAPACITY),則也會用擴容代替紅黑樹。

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

HashMap中的效能優化

HashMap 擴容的時候,不管是連結串列還是紅黑樹,都會對這些資料進行重新的雜湊計算,然後縮短他們的長度,優化效能。在進行雜湊計算的時候,會進一步優化效能,減少減一的操作,直接使用& 運算。

 

HashMap的重新Hash演算法:

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(圖片來自 http://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

避免只靠低位資料來計算雜湊時導致的衝突,計算結果由高低位結合決定,可以避免雜湊值分佈不均勻。

而且,採用位運算效率更高。

HashMap 如何根據 hash 值找到陣列中的物件

    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) {
                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;
    }

關鍵的是其中的這一行:first = tab[(n - 1) & hash])

當n是2的指數時,上面的(n-1)&hash相當於hash%n,對於處理器來說,除法和求餘比較慢,為了效能,使用了減法和按位與運算。

如何正確使用

無論我們如何設定初始容量,HashMap 都會將我們改成2的冪次方,也就是說,HashMap 的容量百分之百是 2的冪次方。

但是,請注意:如果我們預計插入7條資料,那麼我們寫入7,HashMap 會設定為 8,雖然是2的冪次方,但是,請注意,當我們放入第7條資料的時候,就會引起擴容,造成效能損失,所以,知曉了原理,我們以後在設定容量的時候還是自己算一下,比如放7條資料,我們還是都是設定成16,這樣就不會擴容了。

 

非執行緒安全

HashMap 在 JDK 7 中併發擴容的時候是非常危險的,非常容易導致連結串列成環狀。但 JDK 8 中已經修改了此bug。但還是不建議使用。強烈推薦併發容器 ConcurrentHashMap。

編碼啟示

如果參與中介軟體、基礎架構開發,時刻追求效能是很有必要的。

HashMap如何根據指定容量設定閾值

得出最接近指定引數 cap 的 2 的 N 次方容量。假如你傳入的是 5,返回的初始容量為 8 。

/**
     * 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;
    }

 可以自己舉例驗證,但是如果問怎麼寫出來的,我也不知道。只能膜拜。

二進位制位運算規則參考:

<< : 左移運算子,num << 1,相當於num乘以2  低位補0
>> : 右移運算子,num >> 1,相當於num除以2  高位補0
>>> : 無符號右移,忽略符號位,空位都以0補齊
 % : 模運算 取餘
^ :   位異或 第一個運算元的第n位與第二個運算元的第n位相反,那麼結果的第n為也為1,否則為0
 & : 與運算 第一個運算元的第n位與第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0
 | :  或運算 第一個運算元的第n位與第二個運算元的第n位 只要有一個是1,那麼結果的第n為也為1,否則為0
 ~ : 非運算 運算元的第n位為1,那麼結果的第n位為0,反之,也就是取反運算(一元操作符:只操作一個數)

 

參考文章:

深入理解-HashMap-put-方法(JDK-8逐行剖析)

Java 集合深入理解(16):HashMap 主要特點和關鍵方法原始碼解讀

Java:手把手帶你原始碼分析 HashMap 1.7

Java原始碼分析:關於 HashMap 1.8 的重大更新

Java:那些關於集合的知識都在這裡了!

Java 集合深入理解(17):HashMap 在 JDK 1.8 後新增的紅黑樹結構

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析