Java:HashMap原始碼分析

Lesincs發表於2018-03-11

引言

在平時需要用到鍵值對儲存資料時,我們便會用到HashMap,這篇文章基於JDK1.8,參考前輩的文章,對HashMap進行一定的理解.

參考

面試必備:HashMap原始碼解析(JDK8)

HashMap之比於ArrayListLinkedList,感覺原始碼複雜得多,由於才學疏淺,本文很大部分參考上面連結文章,因為註釋寫得很詳細,有幾個函式是照搬的,這裡感謝原作者,以後功力足夠,一定會寫一篇自己的理解。

結構

Java:HashMap原始碼分析

  • 實現了Map介面,對資料進行鍵值對操作
  • 實現了Cloneable介面,可被進行淺拷貝
  • 實現了Serializable介面,可被序列化

Node節點

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //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; }
        //計算節點的hashCode 將key的hashCode值和value的hashCode值異或得到
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //設定新的值,並返回舊的值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //equals方法,兩個節點的key和value都相同時返回true
        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中儲存了下一個節點的引用,是一個單連結串列結構.

建構函式

  • 無參建構函式
    //預設的初始容量為16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
     //預設的載入因子為0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public HashMap() {
    //無參建構函式 使用預設的建構函式
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    複製程式碼
  • 帶初始化容量引數的建構函式
    public HashMap(int initialCapacity) {
    //內部呼叫了另一個建構函式
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    複製程式碼
  • 帶初始化容量容量和載入因子的建構函式
    static final int MAXIMUM_CAPACITY = 1 << 30;
    public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)//初始化容量不能小於0
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)//初始化容量不得大於最大容量
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor)) //載入因子必須是不小於0的浮點數
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor; 
             //設定閾值為 >= initialCapacity 的2的n次方形式的值
            this.threshold = tableSizeFor(initialCapacity);
        }
    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;
    }
    複製程式碼
  • Map型別引數的建構函式
     public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size(); //得到map的元素數量
        if (s > 0) { //數量大於0
            if (table == null) { // 如果表為空
                float ft = ((float)s / loadFactor) + 1.0F; //根據數量計算出閾值
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? 
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold) //如果閾值大於當前閾值 則重新計算閾值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold) //如果當前元素表不是空的,但是s的元素數量大於閾值,說明一定要擴容。
                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()函式 比較重要的擴容函式
    final Node<K,V>[] resize() {
        //oldTab 為當前表的雜湊桶
        Node<K,V>[] oldTab = table;
        //當前雜湊桶的容量 length
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //當前的閾值
        int oldThr = threshold;
        //初始化新的容量和閾值為0
        int newCap, newThr = 0;
        //如果當前容量大於0
        if (oldCap > 0) {
            //如果當前容量已經到達上限
            if (oldCap >= MAXIMUM_CAPACITY) {
                //則設定閾值是2的31次方-1
                threshold = Integer.MAX_VALUE;
                //同時返回當前的雜湊桶,不再擴容
                return oldTab;
            }//否則新的容量為舊的容量的兩倍。 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//如果舊的容量大於等於預設初始容量16
                //那麼新的閾值也等於舊的閾值的兩倍
                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) {
                //取出當前的節點 e
                Node<K,V> e;
                //如果當前桶中有元素,則將連結串列賦值給e
                if ((e = oldTab[j]) != null) {
                    //將原雜湊桶置空以便GC
                    oldTab[j] = null;
                    //如果當前連結串列中就一個元素,(沒有發生雜湊碰撞)
                    if (e.next == null)
                        //直接將這個元素放置在新的雜湊桶裡。
                        //注意這裡取下標 是用 雜湊值 與 桶的長度-1 。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果發生過雜湊碰撞 ,而且是節點數超過8個,轉化成了紅黑樹(暫且不談 避免過於複雜, 後續專門研究一下紅黑樹)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果發生過雜湊碰撞,節點數小於8個。則要根據連結串列上每個節點的雜湊值,依次放入新雜湊桶對應下標位置。
                    else { // preserve order
                        //因為擴容是容量翻倍,所以原連結串列上的每個節點,現在可能存放在原來的下標,即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);
                        //將低位連結串列存放在原index處,
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //將高位連結串列存放在新index處
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    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 {//否則 發生了雜湊衝突。
            //e
            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;
    }
    // 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);
    }
     // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    複製程式碼

總結:

  • 運算儘量都用位運算代替,更高效
  • 對於擴容導致需要新建陣列存放更多元素時,除了要將老陣列中的元素遷移過來,也記得將老陣列中的引用置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重寫使用。

增加元素(改動元素)

  • put(K key,V value)

     public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
        //攪動函式
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    複製程式碼

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

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

刪除元素

  • remove(Object key)
 public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
  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;//將待刪除節點引用賦給node
            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;//修改modCount
                --size;//修改size
                afterNodeRemoval(node);//LinkedHashMap回撥函式
                return node;
            }
        }
        return null;
    }

複製程式碼

查詢元素

  • get()
     public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
        
            //傳入擾動後的雜湊值 和 key 找到目標節點Node
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //查詢過程和刪除基本差不多, 找到返回節點,否則返回null
        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;
    }
    複製程式碼

判斷是否包含某value

  • containsValue(Object value)
    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) {
                        //如果找到value一致的返回true
                        if ((v = e.value) == value ||
                            (value != null && value.equals(v)))
                            return true;
                    }
                }
            }
            return false;
        }
    複製程式碼

總結

  • HashMap採用陣列加連結串列的資料結構儲存資料.
  • 當元素的數量超過閾值,HashMap擴容,擴容數量為2的n次方
  • 允許keynull,valuenull
  • 執行緒不安全,如果需要併發工作使用ConcurrentHashMap

相關文章