Java中的Map【七】HashMap類

      所使用的jdk版本爲1.8.0_172版本,先看一下 HashMap<K,V> 在JDK中Map的UML類圖中的主要繼承實現關係:

概述

       在JDK 1.7 中,HashMap的底層數據結構使用的是 Entry數組 + Entry鏈表,如果HashMap中的key值數hashCode都一樣(極端hash碰撞情況), 那所有數據就會一直都落在同一條Entry鏈表上, 此時設計初衷爲實現快速查找的HashMap就退化爲鏈表, 操作的時間複雜度成爲O(n)。JDK 8 中採用 Node數組 + Node鏈表 + TreeNode 紅黑樹,優化了極端hash碰撞情況下的查詢效率,最壞時間複雜度爲O(log n)。

       HashMap 的實現不是同步的,即線程不安全的。HashMap 允許使用 null 作爲鍵和值

數據結構

Node<K,V>

    static class Node<K,V> implements Map.Entry<K,V> {
        //hash值,hash(key)方法計算得出(key.hashCode和擾動函數得出)
        final int hash;
        //鍵
        final K key;
        //值
        V value;
        //指向下個Node節點的引用
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

TreeNode<K,V>

 /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    /**
     * 紅黑樹節點
     * 紅黑樹性質:
     * 1.節點是紅色或黑色。
     * 2.根節點是黑色。
     * 3.每個葉子節點都是黑色的空節點(NIL節點)。
     * 4 每個紅色節點的兩個子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
     * 5.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        //.......

      注意到 TreeNode<K,V> 繼承了LinkedHashMap 中的 Entry<K,V>,而該 Entry<K,V> 類又繼承了 HashMap 中的Node<K,V>(即上面的 Node<K,V>類):

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

Node<K,V>[]

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    /**
     * Node[]數組table在首次使用時初始化,並且根據需要調節大小。
     * 分配大小後,長度總是2的冪。
     * 長度總是2的冪好處:(n - 1) & hash 計算key將被放置的槽位,n 爲 table 長度;比 %取模運算效率高
     * (在某些操作中,我們還允許長度爲零,以允許使用當前不需要的引導機制。)
     */
    transient Node<K,V>[] table;

構造方法

HashMap 中提供了四種構造方法:

    //默認構造方法,使用默認的加載因子
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    //構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     *  構造一個帶指定初始容量和加載因子的空 HashMap
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //根據初始容量計算數組下一次觸發resize的閾值
        this.threshold = tableSizeFor(initialCapacity);
    }

    //構造一個映射關係與指定 Map 相同的新 HashMap
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

      HashMap觸發數組下一次 resize 擴容操作的閾值 threshold = capacity(容量) * load factor(加載因子),HashMap中的映射元素總數超過threshold時,就會觸發 resize 擴容操作。

tableSizeFor方法

    /**
     * Returns a power of two size for the given target capacity.
     */
    /**
     * 返回一個比給定整數大且最接近的2的冪次方正整數
     * 原理:通過不斷把第一個1開始後面的位變成1,再返回 n + 1,即爲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;
    }

put(K key, V value)方法

    /**
     * 在此映射中關聯指定值與指定鍵。如果該映射以前包含了一個該鍵的映射關係,則舊值被替換。
     *
     * @param key 鍵
     * @param value 值
     * @return 與 key 關聯的舊值;如果 key 沒有任何映射關係,則返回 null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
     */
    public V put(K key, V value) {
        //hash(key):調用擾動函數計算hash值
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 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;
        //通過resize()方法初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n - 1) & hash 相當於 hash % n;如果數組中該位置爲null,即沒有被使用,直接構造新的Node節點放入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//如果數組中的該位置已經被佔用
            Node<K,V> e; K k;
            //如果鏈表的第一個節點或者樹的根節點的key與待放入的key相同,
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//用一個臨時節點e記錄
            //如果數組table中的位置不是該鍵,並且數組中的節點是紅黑樹節點,則按紅黑樹節點處理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果數組table中的位置不是該鍵,並且數組中的節點是鏈表結構節點
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判斷鏈表的長度是否達到了 8 個
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        /*如果數組的長度沒有超過64,則進行擴容操作;
                         *如果數組長度超過64,則把衝突的鏈表結構轉換爲紅黑樹結構*/
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果鏈表中已經存在了待插入的key,跳出循環
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //key已經存在的情況,用新值替換舊值,返回舊值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //回調方法,提供給 LinkedHashMap 後處理的回調
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果HashMap中映射的數量達到了閾值,則觸發擴容操作
        //如果數據量很大,擴容的時間開銷不可忽略,要考慮到。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//提供給LinkedHashMap使用的回調,這裏爲空實現
        return null;
    }

treeifyBin方法

當鏈表長度大於 8 的時候,會進到terrifyBin()方法中:

    /** 
     * 前提:鏈表的長度超過了 8  
     * 如果數組的長度沒有超過64,則進行擴容操作;
     * 如果數組長度超過64,則把衝突的鏈表結構轉換爲紅黑樹結構
     */
    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);
        }
    }

擴容方法 resize()

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) {
            //擴容前數組長度超過最大值,2^30;此時數組已經很大,不再擴容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果舊數組長度超過了默認容量16,並且舊數組長度的二倍不超過 2^30
            //則把新數組長度設爲舊數組長度二倍
            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
            //默認構造方法創建HashMap的情況
            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
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //擴容後,對新擴容後的table賦值,重新計算元素新的位置
        //擴容時數組長度由2的n次冪變爲2的n+1次冪, 而hash根據2的n+1次冪取模的值即是hash轉爲二進制後的後n位,
        // 所以如果hash的第n位(可用hash & 舊數組長度計算)爲0的話數據在擴容後新數組中的位置就和在舊數組中的位置相同, 爲1的話數據在擴容後新數組中的位置就是(原位置+舊數組長度位置).
        // 擴容時舊數組在 j 位置上的數據會根據hash的第n位是0還是1拆分爲2組, 然後把爲0的放到新數組的j位置, 爲1的放到 j+舊數組長度位置
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判斷當前遍歷下的該node是否爲空,將j位置上的節點保存到e, 然後將oldTab[j]置爲空。
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //舊數組上的普通節點(沒有後綴節點),根據e.hash & (newCap - 1)計算出它在新數組上的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //舊數組上節點已經是紅黑樹節點的情況:
                    //把舊數組的紅黑樹節點數據按照上面的邏輯計算位置(得到兩個紅黑樹)重新樹化,
                    //如果新樹的節點數量 <= UNTREEIFY_THRESHOLD (默認爲 6),則把樹結構降爲鏈表
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //舊數組節點爲鏈表節點的情況:
                        //上面所述,用e.hash & oldCap得到hash值的第n位,拆成爲0和1的兩個鏈表,對應放到原位置j和新位置(j + oldCap)
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

 

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