1. 程式人生 > >Map容器家族(HashMap原始碼詳解)

Map容器家族(HashMap原始碼詳解)

一、在Map集合家族的位置及描述

        HashMap子類繼承自AbstractMap抽象類,實現了Map,Serializable,Cloneable介面,AbstractMap實現了Map介面的一部分方法,減輕了其子類的負擔。

        概括的說,HashMap底層儲存元素的資料結構是雜湊(hash)也稱為雜湊表(陣列+連結串列),雜湊表的資料結構是陣列和連結串列,陣列在HashMap中有稱之為雜湊桶,每個桶裡面存放的是連結串列(這就是雜湊表),連結串列的每個節點就是雜湊表中的每個元素。HashMap它是執行緒不安全的,允許鍵為空,值為空。在JDK8中,當連結串列的長度達到8時,連結串列會轉換成紅黑樹,以提升它的查詢和插入效率。

        其底層資料結構是陣列,所以會涉及到擴容問題。當HashMap的容量達到閥值threshold時,就會觸發擴容操作。擴容前後,雜湊桶的長度一定會是2的次方大小。這樣根據key的hash值尋找地址時就可以運用為運算代替代替效率低的模運算。

        key的hash值,並不僅僅只是key物件的hashCode()方法的返回值,還會經過擾動函式的擾動,以使hash值更加均衡。 因為hashCode()是int型別,取值範圍是40多億,只要雜湊函式對映的比較均勻鬆散,碰撞機率是很小的。 但就算原本的hashCode()取得很好,每個key的hashCode()不同,但是由於HashMap的雜湊桶的長度遠比hash取值範圍小,預設是16,所以當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,由於取餘是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之為hash碰撞。 即,碰撞率會增大。 擾動函式就是為了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,因此在與運算時,相當於高低位一起參與了運算,以減少hash碰撞的概率。(在JDK8之前,擾動函式會擾動四次,JDK8簡化了這個操作) 擴容操作時,會new一個新的Node陣列作為雜湊桶,然後將原雜湊表中的所有資料(Node節點)移動到新的雜湊桶中,相當於對原雜湊表中所有的資料重新做了一個put操作。所以效能消耗很大,可想而知,在雜湊表的容量越大時,效能消耗越明顯。

        下面將從成員變數,構造方法,常用API(增刪改查)的順序去閱讀原始碼,在閱讀中遇到演算法時,將拿出來,詳細的講解。

二、儲存元素的資料結構

1.連結串列的節點

在開始之前,我們先看一下掛載在雜湊表上的元素,連結串列的結構:

    /**
     * 基本雜湊bin節點,用於大多數條目。
     * (參見下面的TreeNode子類,以及LinkedHashMap中的Entry子類。)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;     // 雜湊值
        final K key;        // key
        V value;        // value
        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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        // 每一個節點的hash值,是將key的hashCode和value的hashCode異或得到的。
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        // 設定新的value 同時返回舊value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        /**
         * 比較引數的Entry鍵值對是否和該鍵值對相等
         */
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

根據Node節點和雜湊方法知道,該節點的雜湊值是鍵和值的雜湊值異或得到的。

三、成員變數

    // 預設初始容量 1 << 4 = 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 最大容量 2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 預設載入因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 轉換成紅黑樹的閥值
    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;

    // 雜湊桶,用於存放連結串列;長度時2的N次方,目的是用位移運算取代除法
    transient Node<K,V>[] table;

    // 儲存鍵值對
    transient Set<Map.Entry<K,V>> entrySet;

    // 包含在map中的鍵值對個數
    transient int size;

    // 修改次數
    transient int modCount;

    // 雜湊表內元素數量的閥值,當雜湊表元素數量超過閥值時,會發生擴容resize()
    int threshold;

    // 載入因子,用於計算雜湊表元素數量的閥值。threshold = 雜湊桶.length * loadFactor
    final float loadFactor;

四、構造方法

    // 預設建構函式,賦值載入因子為預設的0.75f
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    // 指定初始化容量的建構函式
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 同時指定初始化容量 以及 載入因子, 用的很少,一般不會修改loadFactor
    public HashMap(int initialCapacity, float loadFactor) {
        // 邊界處理
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);

        // 初始容量最大不能超過2的30次方
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //顯然載入因子不能為負數
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //設定閾值為  初始化容量的2的n次方的值
        this.threshold = tableSizeFor(initialCapacity);
    }

    // 將指定map中的所有元素新增到本集合中
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 新增方法
        putMapEntries(m, false);
    }

構造器中呼叫的方法:

    static final int tableSizeFor(int cap) {
        // 經過下面的 或 和位移 運算, n最終各位都是1。
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 判斷n是否越界,返回2的n次方作為table(雜湊桶)的閾值
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    /**
     * 功能:將另一個Map的所有元素加入表中
     * evict引數:最初構造此對映時為false,否則為true
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        // 拿到m的元素數量
        int s = m.size();
        if (s > 0) {        // 如果數量大於0
            if (table == null) {        // 如果當前表是空的
                // 根據m的元素數量和當前表的載入因子,計算出閾值
                float ft = ((float)s / loadFactor) + 1.0F;
                //修正閾值的邊界 不能超過MAXIMUM_CAPACITY
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);

                //如果新的閾值大於當前閾值
                if (t > threshold)
                    //返回一個>=新的閾值的,滿足2的n次方的閾值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)     // 如果當前元素表不是空,且m的元素數量大於閾值,說明一定要擴容
                resize();       // 擴容
            // 遍歷 m 依次將元素加入當前表中。
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

擴容函式resize(): 這是一個重點!重點!重點!
**初始化或加倍雜湊桶大小。如果是當前雜湊桶是null,分配符合當前閾值的初始容量目標。否則,因為我們擴容成以前的兩倍。在擴容時,要注意區分以前在雜湊桶相同index的節點,現在是在以前的index裡,還是index+oldlength 裡** 

    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) {   // 如果當前容量大於0
            if (oldCap >= MAXIMUM_CAPACITY) {   // 如果當前容量達到上限
                threshold = Integer.MAX_VALUE;  // 設定閥值為最大
                return oldTab;  // 返回現在的雜湊桶,不擴容
            }
            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;  //此時新表的容量為預設的容量 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的閾值為預設容量16 * 預設載入因子0.75f = 12
        }

        if (newThr == 0) {  // 如果新的閾值是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]; // 根據新的容量 構建新的雜湊桶
        table = newTab; // 更新雜湊桶引用
        // 如果以前的雜湊桶中有元素
        // 下面開始將當前雜湊桶中的所有節點轉移到新的雜湊桶中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {  // 遍歷老的雜湊桶
                Node<K,V> e;    // 建立節點引用
                if ((e = oldTab[j]) != null) {  // 如果當前桶中有元素,則將連結串列賦值給e
                    oldTab[j] = null;   // 將原雜湊桶置空以便GC
                    if (e.next == null) //如果當前連結串列中就一個元素(沒有發生雜湊碰撞)
                        // 直接將這個元素放置在新的雜湊桶裡
                        // 注意這裡取下標是用 雜湊值 與 桶的長度-1。由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 如果發生過雜湊碰撞 ,而且是節點數超過8個,轉化成了紅黑樹(暫且不談 避免過於複雜, 後續專門研究一下紅黑樹)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order // 如果發生過雜湊碰撞,節點數小於8個。則要根據連結串列上每個節點的雜湊值,依次放入新雜湊桶對應下標位置。
                        // 因為擴容是容量翻倍,所以原連結串列上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位=low位+原雜湊桶容量
                        // 低位連結串列的頭結點、尾節點
                        Node<K,V> loHead = null, loTail = null;
                        // 高位連結串列的頭節點、尾節點
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next; // 臨時節點 存放e的下一個節點
                        do {
                            next = e.next;
                            // 這裡又是一個利用位運算 代替常規運算的高效點: 利用雜湊值 與 舊的容量,可以得到雜湊值去模後,
                            // 是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位
                            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;
    }

再看一下 往雜湊表裡插入一個節點的putVal函式,如果引數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。如果evict是false。那麼表示是在初始化時呼叫的

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab存放當前的雜湊桶,p用作臨時連結串列節點
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果當前雜湊表是空的,代表是初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            // 那麼直接去擴容雜湊表,並且將擴容後的雜湊桶長度賦值給n
            n = (tab = resize()).length;
        // 如果當前index的節點是空的,表示沒有發生雜湊碰撞。 直接構建一個新節點Node,掛載在index處即可。
        // 這裡再囉嗦一下,index 是利用 雜湊值 & 雜湊桶的長度-1,替代模運算
        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;//將當前節點引用賦值給e
            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,則轉化為紅黑樹
                        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;
                }
            }
            //如果e不是null,說明有需要覆蓋的節點
            if (e != null) { // existing mapping for key
                //則覆蓋節點值,並返回原oldValue
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //這是一個空實現的函式,用作LinkedHashMap重寫使用。
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //如果執行到了這裡,說明插入了一個新的節點,所以會修改modCount,以及返回null。
        //修改modCount
        ++modCount;
        //更新size,並判斷是否需要擴容。
        if (++size > threshold)
            resize();
        //這是一個空實現的函式,用作LinkedHashMap重寫使用。
        afterNodeInsertion(evict);
        return null;
    }

newNode()如下,構建一個連結串列節點

    // Create a regular (non-tree) node
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

小結:
* 運算儘量都用位運算代替,更高效
* 對於擴容導致需要新建陣列存放更多元素時,除了要將老陣列中的元素遷移過來,也記得將老陣列中的引用置null,以便GC
* 取下標 是用 雜湊值 與運算 (桶的長度-1) i = (n - 1) & hash。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
* 擴容時,如果發生過雜湊碰撞,節點數小於8個。則要根據連結串列上每個節點的雜湊值,依次放入新雜湊桶對應下標位置。
* 因為擴容是容量翻倍,所以原連結串列上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原雜湊桶容量
* 利用雜湊值 與運算 舊的容量if ((e.hash & oldCap) == 0),可以得到雜湊值去模後,是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位。這裡又是一個利用位運算 代替常規運算的高效點
* 如果追加節點後,連結串列數量》=8,則轉化為紅黑樹
* 插入節點操作時,有一些空實現的函式,用作LinkedHashMap重寫使用。 

五、常用API

1.新增元素(修改操作)

    /**
     * 功能:向map中新增元素
     * 實現:呼叫之前講的putVal方法
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 功能:向map中新增指定map集合的元素(批量新增資料)
     * 實現:呼叫之前講的putMapEntries方法
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

這個根據key取hash值的函式也要關注一下,它稱之為“擾動函式”,關於這個函式的用處 開頭已經總結過了:

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

而key的hash值,並不僅僅只是key物件的hashCode()方法的返回值,還會經過擾動函式的擾動,以使hash值更加均衡。
因為hashCode()int型別,取值範圍是40多億,只要雜湊函式對映的比較均勻鬆散,碰撞機率是很小的。
但就算原本的hashCode()取得很好,每個key的hashCode()不同,但是由於HashMap的雜湊桶的長度遠比hash取值範圍小,預設是16,所以當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,由於取餘是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之為hash碰撞。 即,碰撞率會增大。

擾動函式就是為了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,因此在與運算時,相當於高低位一起參與了運算,以減少hash碰撞的概率。(在JDK8之前,擾動函式會擾動四次,JDK8簡化了這個操作)

JDK1.8新新增的方法:

    /**
     * 功能:新增指定的鍵值對,若之前值不存在會覆蓋,存在就不會覆蓋
     */
    @Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }

2.刪除操作

根據鍵刪除鍵值對,並返回被刪除的鍵值對的值

    /**
     * 功能:根據鍵刪除鍵值對
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

從雜湊表中刪除某個節點, 如果引數matchValue是true,則必須key 、value都相等才刪除。 
如果movable引數是false,在刪除節點時,不移動其他節點

    /**
     * @param hash  鍵對應雜湊值
     * @param key   鍵
     * @param value 值
     * @param matchValue    如果是true,只有當鍵值對都相等時候移除
     * @param movable   如果是false,在刪除節點時候不移動其他節點
     * @return  返回移除的節點
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        // p 是待刪除節點的前置節點
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 如果雜湊表不為空,則根據hash值算出的index
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            // node是待刪除節點
            Node<K,V> node = null, e; K k; V v;
            //如果連結串列頭的就是需要刪除的節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {//否則迴圈遍歷 找到待刪除節點,賦值給node
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果有待刪除節點node,且matchValue為false,或者值也相等
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)//如果node ==  p,說明是連結串列頭是待刪除節點
                    tab[index] = node.next;
                else//否則待刪除節點在表中間
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

以鍵和值為條件刪除鍵值對

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }

3.查詢操作

以鍵之條件查詢值

    /**
     * 功能:根據鍵獲取值
     */
    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) {
                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;
    }

4.判斷包含與否

    /**
     * 功能:判斷是否包含指定的key
     */
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

JDK8新新增的方法

以key為查詢條件,找到返回value,否者返回defaultValue

    @Override
    public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }

5.遍歷操作

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        //一般我們用到EntrySet,都是為了獲取iterator
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

        // 最終還是呼叫getNode方法
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }

        //最終還是呼叫removeNode方法
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        // 還有其他方法
   }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            //因為hashmap也是執行緒不安全的,所以要儲存modCount。用於fail-fast策略
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //next 初始時,指向 雜湊桶上第一個不為null的連結串列頭
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        // 由這個方法可以看出,遍歷HashMap時,順序是按照雜湊桶從低到高,連結串列從前往後,依次遍歷的。屬於無序集合。
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            // 依次取連結串列下一個節點
            if ((next = (current = e).next) == null && (t = table) != null) {
                //如果當前連結串列節點遍歷完了,則取雜湊桶下一個不為null的連結串列頭
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

六、總結

        HashMap資料結構是  陣列 + 連結串列   構成雜湊表  使用的是避免雜湊衝突的拉鍊法。在Jdk1.8中連結串列引入了紅黑樹的概念,即連結串列的閾值大於等於7時會轉變成紅黑樹的結構,即桶裡裝了棵紅黑樹。

        儲存方式計算h=鍵.hashCode,返回 h  = h ^ (h >>> 16) ,通過這個返回值 h & (n - 1)得到對應的下標值。找到相應的桶,檢視桶中是否有一樣的鍵,有就更新鍵值。沒有的話,如果是連結串列就在尾部插,是紅黑樹就就找到固定位置插入,回溯維護紅黑樹的性質。

        紅黑樹樹化條件,連結串列樹化要大於等於7(treeifyBin()函式),還原要小於6 (untreeify()函式)。

        關於併發的操作,我們最好還是使用ConcurrentHashMap。