HashMap源碼解析-java8

1.構造函數如下

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m)

2.底層實現思想

(1) 基於數組和鏈表實現,拉鍊法,數組存在擴容不存在縮容,鏈表在java8里加入了紅黑樹結構,見下文

(2) 在通過迭代器遍歷HashMap的Node過程中,如果進行了結構性的更改,會fail-fast快失敗,會拋出ConcurrentModificationException,api doc講到依賴這個異常去糾錯是不合理的,而是應該僅僅是去發現bugs

一、常見問題解答(大多是api doc上的原話或是看源碼自己的總結)

1.擴容機制

翻倍擴容,容量變成原來的兩倍,默認容量爲16,也可初始指定容量,初始化的時候,閾值threshold爲capacity,也就是說第一次threshold並不是等於capacity*loadFactor,當元素個數到達threshold時會擴容,即resize

2.負載因子爲什麼默認取0.75

負載因子高,空間利用率高,但是查詢效率低,容易哈希碰撞。負載因子低,空間利用低,浪費空間。因此折中
另外,只有哈希碰撞嚴重時,纔會出現紅黑樹結構,紅黑樹節點佔用空間大約是普通節點2倍,實際上很少出現
理想情況隨機哈希下,節點bucket上出現node個數和概率滿足泊松分佈,當負載因子是0.75時,泊松分佈的lambda值是0.5,每個bucket鏈表節點node出現的個數和概率滿足一個公式(見api doc)
當出現8-9個node時的概率,算法理想上只有千萬分之1,即 1 in ten million

3.哈希函數是怎麼設計的,哈希是怎麼定址的

使用key.hashCode的高16位保持不變,低16位爲高16位和低16位的異或,即(h = key.hashCode()) ^ (h >>> 16)
原因:通常的hashCode函數已經足夠合理分佈了,我們沒有必要打亂他的節奏,考慮到位運算的便捷和快速,減少系統損耗,容量又是2的冪,哈希值只是比特位不一樣,因此我們用高位和低位異或,加入對高位的影響
而哈希碰撞用紅黑樹處理。哈希定址採用哈希值對容量取模,源碼中是通過與capacity-1進行位運算,因爲位運算快

4.如果哈希碰撞嚴重,可能有哪些原因

可能是重寫Key的哈希函數設計的不合理,儘量用Objects.hash即可,系統自帶的。再就可能是負載因子設置的過高

5.鏈表和紅黑樹轉換的規則是什麼樣的

每個桶上鍊表元素個數>=8個元素則轉換爲紅黑樹存儲(並且滿足capacity>=64),減少到<=6個又變回鏈表結構(擴容時,可能一條鏈變兩條鏈,所以元素個數會減少),刪除結點時,變回鏈表的觸發條件因樹的結構而異,此時樹大概只有2-6個node,源碼中的條件是root的左兒子的左兒子爲空,具體源碼註釋有講到

6.java7的HashMap和java8的HashMap有哪些區別

java8源碼就增加了幾千行,加入了很多默認函數,lambda等,更重要的是java7插入節點使用頭插法(會產生環形鏈表死循環問題)和java8使用尾插法,哈希定址計算方式也不一樣,並且java8引入了紅黑樹,這是java7不具備的。

7.java8的HashMap爲什麼也不是線程安全的

resize函數就是不安全的,還沒複製完,另一個線程訪問,此時table部分bin爲null,源碼中的處理是先開闢新數組,再複製元素。
put的時候也會出現問題,bin有值,然而讀不到,本該形成鏈表,結果覆蓋了另一個線程新put的值
size變量也不是volatile線程可見的,而且有++size操作

8.HashMap裏結點Node的結構是什麼樣的

Map.Entry是Map接口裏的public內部子接口
HashMap.Node是普通鏈表節點,是內部類,它實現了Map.Entry
HashMap.TreeNode是紅黑樹節點,是final內部類,它繼承了LinkedHashMap.Entry,而後者又繼承了HashMap.Node

9.Hashtable和HashMap的區別

一個區別是線程安全性,一個是key和value是否可以爲null

10.HashMap.Node的hash屬性和key屬性爲什麼是final的

因爲不能改變,hash不用重複計算,節約計算代價

11.紅黑樹的排序規則是怎麼樣的

紅黑樹的排序首先使用hash值比較,其次是Key的compareTo方法(判斷實現Comparable接口),再是類名字符串等(如通過反射)和identityHashCode值排序,構造紅黑樹

二、源碼解析

1.哈希計算方法

/**
 * 哈希計算規則
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2.容量capacity計算規則

/**
 * 打成2的冪,這就是位運算的魅力
 */
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;
}

3.put方法底層原理

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //該bin是空,直接添加,注意哈希定址是位運算,i = (n - 1) & hash
        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))))
            //key已存在,直接找到,就替換
            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;
    if (++size > threshold)
        //到達閾值,擴容
        resize();
    afterNodeInsertion(evict);
    return null;
}

4.擴容函數

/**
 * 擴容機制
 * @return the table
 */
final Node<K,V>[] resize() {
    //舊數組
    Node<K,V>[] oldTab = table;
    //舊容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //舊閾值
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //位運算capacity進行翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//開闢新數組
    //記住,此時數組替換了,node元素還沒過來
    table = newTab;
    if (oldTab != null) {
        //開始把舊數組的node元素複製到新數組中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //清理內存
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //紅黑樹結構的擴容
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order保持秩序順序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //敲黑板,這裏只判斷最高位,如果不爲0,那麼hash值大於舊的容量,要放到高位的鏈表中,
                        //這就是擴容爲什麼一條鏈表可能變2條的原因
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        //魅力之處,直接+oldCap定址,這是與java7的一個不同點
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回新開闢的數組
    return newTab;
}

5.把hashmap對應哈希值位置的bucket變成紅黑樹

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);
        //最後調用頭結點的treeify方法,將其轉變爲具備父子關係的紅黑樹結構
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

三、紅黑樹結點內部的方法

1.把樹的root放到鏈表的頭

/**
 * 把root放到鏈表的頭部去
 */
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        if (root != first) {
            Node<K,V> rn;
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

2.紅黑樹的二叉查找

/**
 * 從當前TreeNode往子孫節點搜索k
 */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        //紅黑樹是二叉有序的,二分搜索,log(n)複雜度
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        //首先比較哈希值,
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            //直接找到
            return p;
        else if (pl == null)
            //如果左邊null,則到右邊搜
            p = pr;
        else if (pr == null)
            //如果右邊null,則到左邊搜
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            //如果能通過compareTo判斷,則這樣繼續搜索
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            //否則類似遞歸,繼續find從右兒子搜索,直到找到
            return q;
        else
            //如果右兒子搜索沒搜到,繼續從左兒子往下搜
            p = pl;
    } while (p != null);
    return null;
}

3.建立紅黑樹,key怎麼比較大小

//首先通過哈希值比較,然後通過key的compareTo方法,如果都無法比較大小,那麼採用下面的方法比較大小
static int tieBreakOrder(Object a, Object b) {
    int d;
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        //如果通過類名字符串無法區分,用identityHashCode應該能區分吧,這是內存級別的
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}

4.把紅黑樹打成鏈表

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        //把樹節點轉換爲普通鏈表節點,然後next串起來,prev之前有值不變
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    //返回頭節點
    return hd;
}
//真的佩服源碼的規範性,一般t代表temp臨時變量,l代表左邊,r代表右邊,h代表head或high
//還有比如,hd代表head,tail代表尾巴,tl代表temp臨時變量,等等

5.紅黑樹刪除節點(不包含顏色調整),先看圖解,如果不懂原理請後續關注我的紅黑樹基礎-第二篇

這個函數主要是this是要刪除的節點,找到節點s與之互換,然後用balanceDeletion調整,看下圖中樹結構的變化再看代碼註釋

/**
 * Removes the given node, that must be present before this call.
 * This is messier than typical red-black deletion code because we
 * cannot swap the contents of an interior node with a leaf
 * successor that is pinned by "next" pointers that are accessible
 * independently during traversal. So instead we swap the tree
 * linkages. If the current tree appears to have too few nodes,
 * the bin is converted back to a plain bin. (The test triggers
 * somewhere between 2 and 6 nodes, depending on tree structure).
 * @param map 該hashmap
 * @param tab hashmap內部Node數組
 * @param movable 是否需要移動root到鏈表頭
 * @param this 待刪除節點p
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
    int index = (n - 1) & hash;
    //root是根節點
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    //分別是this=p的鏈表指針的前驅和後繼
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    if (pred == null)
        //如果要刪除的p是鏈表的頭,那麼first = succ;並且tab[index] = succ;
        tab[index] = first = succ;
    else
        //否則鏈表斷開this的鏈接
        pred.next = succ;
    if (succ != null)
        //鏈表斷開this的鏈接,把鏈表關係完善
        succ.prev = pred;
    if (first == null)
        //空樹
        return;
    if (root.parent != null)
        //獲取到真正的根root
        root = root.root();
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        //根的左孩子的左孩子爲空,基本上可以判斷只剩2-6個node了,紅黑樹可以變成鏈表了
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    //主要關注這種左右孩子都非空的場景,爲什麼這種這麼複雜,請看我寫的關於紅黑樹基礎的其他博客
    if (pl != null && pr != null) {
        //請看圖解,我隨便畫了個圖,節點名和變量名一致
        TreeNode<K,V> s = pr, sl;
        //先找到大於刪除節點p的最小節點,爲什麼這麼做,請看紅黑樹基礎-第3篇
        while ((sl = s.left) != null) // find successor
            s = sl;
        //首先互換p節點和s節點的顏色,因爲最終s要被換到p的位置,p要被換到s的位置
        //互換後,在p位置的s因爲是p的顏色,所以不影響紅黑樹的性質
        //而換到s位置的p,顏色是原s節點的顏色
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        if (s == pr) { // p was s's direct parent
            //此時s==sp==pr,其實是建立p和s的關係,將else的內容簡化了
            p.parent = s;
            s.right = p;
        }
        else {
            TreeNode<K,V> sp = s.parent;
            //建立p和sp的新父子關係,大家可以畫圖分析
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            //建立s和pr的新父子關係
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        //建立p和sr的新父子關係
        if ((p.right = sr) != null)
            sr.parent = p;
        //建立s和pl的新父子關係
        if ((s.left = pl) != null)
            pl.parent = s;
        //建立s和pp的新父子關係
        //如果pp爲空,之前p就是根節點,那麼現在s就是根節點了
        if ((s.parent = pp) == null)
            root = s;
        else if (p == pp.left)
            //如果原來p是pp的左孩子,互換後s就還是pp左孩子
            pp.left = s;
        else
            pp.right = s;
        //如果sr不爲空,則互換後p有右孩子,沒有左孩子,
        if (sr != null)
            //單鏈接情況直接用孩子替換
            replacement = sr;
        else
            //此時p沒有孩子
            replacement = p;
    }
    //單鏈接情況,單單隻有左孩子
    else if (pl != null)
        replacement = pl;
    //單鏈接情況,單單隻有右孩子
    else if (pr != null)
        replacement = pr;
    else
        //p是葉子結點
        replacement = p;
    //互換後,如果p不是葉子結點,在樹結構中直接把p節點幹掉
    if (replacement != p) {
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        p.left = p.right = p.parent = null;
    }

    //回到紅黑樹的平衡刪除了,如果要刪除的節點是紅色,那麼直接刪除即可
    //如果p是黑色的,那麼此時就不滿足紅黑樹的性質了,因爲少了一個黑色節點,那麼要進行balanceDeletion調整
    //注意:p的顏色是原來s的顏色
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

    //那麼如果p是葉子結點,在樹結構中直接把p節點幹掉,detach斷開連接
    if (replacement == p) {  // detach
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    //是否需要把root放到鏈表head
    if (movable)
        moveRootToFront(tab, r);
}

6.其他部分函數未完待續

紅黑樹源碼部分(左右旋轉,平衡插入和平衡刪除請看我第三篇紅黑樹源碼解析

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