1. 程式人生 > >基於jdk1.8的HashMap原始碼學習筆記

基於jdk1.8的HashMap原始碼學習筆記

      作為一種最為常用的容器,同時也是效率比較高的容器,HashMap當之無愧。所以自己這次jdk原始碼學習,就從HashMap開始吧,當然水平有限,有不正確的地方,歡迎指正,促進共同學習進步,就是喜歡程式設計師這種開源精神。(好吧,第一篇部落格有點緊張)

一. HashMap結構

      HashMap在jdk1.6版本採用陣列+連結串列的儲存方式,但是到1.8版本時採用了陣列+連結串列/紅黑樹的方式進行儲存,有效的提高了查詢時間,解決衝突。這裡有一篇部落格寫的非常好,HashMap的結構圖也畫的非常清楚,鼎力推薦一下杭州Mark的HashMap原始碼分析。雖然是基於jdk1.6的,但是換成jdk1.8,只要連結串列長度大於門限時,換成紅黑樹就好了, 我就不具體解釋HashMap的具體結構了。

二.HashMap 定義

public class HashMap<K,V> extends AbstractMap<K,V>
   implements Map<K,V>, Cloneable, Serializable

      繼承自AbstractMap,這是一個抽象類,定義了並且實現了Map介面的基本行為,但是基本上HashMap都對其進行了重寫覆蓋,採用了自己更高效的方式。AbstractMap是我們自己編寫Map時,可以用到的基類。(好吧,來自JAVA程式設計思想,還沒有自己寫過Map)

      實現了Map介面,這裡其實有一個疑問,為什麼在AbstractMap中已經明顯實現Map介面的情況下,還要顯著在實現Map介面?這種設計方式的原因?
      其餘都是標記介面。

三.重要的field屬性

//預設的初始化陣列大小為16,為啥不直接寫16(jdk1.6是這麼幹的)啊?confused
  //這裡必須是2的整數冪,
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
  //最大儲存容量
  static final int MAXIMUM_CAPACITY = 1 << 30
  //預設裝載因子,我理解的就是所佔最大百分比,即當超過75%時,就需要擴容了
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
  //這是jdk1.8新加的,這是連結串列的最大長度,當大於這個長度時,就會將連結串列轉成紅黑樹
static final int TREEIFY_THRESHOLD = 8
; //table就是儲存Node 的陣列,就是hash表中的桶位,後面會重點介紹Node transient Node<K,V>[] table; //實際儲存的數量,則HashMap的size()方法,實際返回的就是這個值,isEmpty()也是判斷該值是否為0 transient int size; //HashMap每改變一次結構,不管是新增還是刪除都會modCount+1,主要用來當迭代時,保持資料的一致性(不知道這麼理解正確麼?) //因為每一次迭代,都會檢查modCount是否改變是否一致,不一致就會丟擲異常。這也是為什麼迭代過程中,除了運用迭代器的remove()方法外,不能自己進行改變資料 //fast-fail機制 transient int modCount; //擴容的門限值,當大於這個值時,table陣列要進行擴容,一般等於(cap*loadFactor) int threshold; //裝載因子 final float loadFactor;

四.HashMap構造器

     HashMap共有四種構造器,常用的有三種,主要分為無參構造器,應用預設的引數;指定初始化容量的構造器;將原有Map裝進當前HashMap的構造器。

1.無參構造器

//這就是一個預設的方式,潛在的問題是初始容量16太小了,可能中間需要不斷擴容的問題,會影響插入的效率,當看到後面resize()方法時,可以很明顯的看到這個問題
   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

2.含參包括裝載因子,與容量

//可以指定初始容量,以及裝載因子,但是感覺一般情況下指定裝載因子意義不大
    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;
        this.threshold = tableSizeFor(initialCapacity)//這裡就重新定義了擴容的門限
    }

           可以看到此時threshold被tableSizeFor()方法重新計算了,那我們研究一下tableSizeFor方法

//tableSizeFor的功能主要是用來保證容量應該大於cap,且為2的整數冪,但是這段程式碼沒有完全看懂
    //我理解了一下就是將最高位依次跟後面的進行或運算,將低位全變成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;
    }

      不得不說寫原始碼的都是追求完美的大神,下面是jdk1.6的實現方式,就是簡單的迴圈,可以看到採用位運算後效率明顯提高了      

int capacity = 1;
 while (capacity < initialCapacity)
            capacity <<= 1;

      這裡可能還有一個疑問,明明給的是初始容量,為什麼要計算門限,而不是容量呢?其實這也是jdk1.8的改變,它將table的初始化放入了resize()中,而且壓根就沒有capacity這個屬性,所以這裡只能重新計算threshold,而resize()後面就會根據threshold來重新計算capacity,來進行table陣列的初始化,然後在重新按照裝載因子計算threshold,有點繞,後面看resize()原始碼就清楚了。

3.僅僅指定容量

//下面這種方式更好,所以當知道所要構建的資料容量大小時,最好直接指定大小,可以減除不停擴容的過程,大幅提高效率
   public HashMap(int initialCapacity){
       this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }

4.將已有Map放入當前map中

//這種方式是將已有Map中的元素放入當前HashMap中
   public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
//這裡就是一個個取出m中的元素呼叫putVal,一個個放入table中的過程。
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                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)
                resize();
            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);
            }
        }
    }

五.重要的方法

1.Node 內部類

     可以看到前面table就是Node陣列,Node是什麼?它是一個HashMap內部類(改名字了,jdk1.6就叫Entry),繼承自 Map.Entry這個內部介面,它就是儲存一對對映關係的最小單元,也就是說key,value實際儲存在Node中。

//可以看出雖然這裡不叫Entry這個廣泛應用的名字了,但是與jdk1.6比,還是沒有什麼大的變化
  //依然實現了Map.Entry這個內部介面,換湯沒換藥
//其實就是最基本的連結串列實現方式
  static class Node<K,V> implements Map.Entry<K,V> {
       //可以發現,HashMap的每個鍵值對就是靠這裡實現的,並沒有很高大上
      //並且鍵應用了final修飾符,所以鍵是不可改變的!
        final int hash;//這裡這個一般儲存鍵的hash值
        final K key;
        V value;
        //next 應用於一個桶位中的連結串列結構,表示下一個節點鍵值對
        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與值的hashcode異或,得到hash值,並且這裡應用的是最原始的hashCode計算方式,Objects
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                //鍵和值都eauals,才行,而且是Objects.equals,只能是同一物件
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }

2.put方法

//可以看到是呼叫的putVal的方法,並且計算了key的hash值
   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
// Computes key.hashCode() and spreads (XORs) higher bits of hash
     /** to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        //這裡可以看到key是有可能是null的,並且會在0桶位位置
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

      這裡hashCode的計算也進行了改進,取得key的hashcode後,高16位與低16位異或運算重新計算hash值。

     下面重點看一下put方法實現的大拿,putVal方法

/**
     * 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;
        //table 沒有初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            //可以看到table的初始化放在了這裡,是通過resize來做的,後面會分析resize()
            n = (tab = resize()).length;

        //這裡就是HASH演算法了,用來定位桶位的方式,可以看到是採用容量-1與鍵hash值進行與運算
        //n-1,的原因就是n一定是一個2的整數冪,而(n - 1) & hash其實質就是n%hash,但是取餘運算的效率明顯不如位運算與
        //並且(n - 1) & hash也能保證雜湊均勻,不會產生只有偶數位有值的現象
        if ((p = tab[i = (n - 1) & hash]) == null)
            //當這裡是空桶位時,就直接構造新的Node節點,將其放入桶位中
            //newNode()方法,就是對new Node(,,,)的包裝
    //同時也可以看到Node中的hash值就是重新計算的hash(key)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //鍵hash值相等,鍵相等時,這裡就是發現該鍵已經存在於Map中
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //當該桶位是紅黑樹結構時,則應該按照紅黑樹方式插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//當該桶位為連結串列結構時,進行連結串列的插入操作,但是當連結串列長度大於TREEIFY_THRESHOLD - 1,就要將連結串列轉換成紅黑樹
            else {
                //這裡binCount記錄連結串列的長度
                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;
                }
            }
            //替換操作,onlyIfAbsent為false
            //所以onlyIfAbsent,這個引數主要決定是否執行替換,當該鍵已經存在時,
              //而下面的方法則是一種不替換的put方法,因為onlyIfAbsent為true
            //這裡其實只是為了給putIfAbsent方法提供支援,這也是jdk1.8新增的方法
              // public V putIfAbsent(K key, V value) {
            //          return putVal(hash(key), key, value, true, true);
            //      }
           //    
            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;
    }

     下面在看一下擴容方法resize(),jdk1.8的resize相比以往又多了一份使命,table初始化部分,也會在這裡完成。

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new 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;
            }
            //採用二倍擴容
            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) {
            //這裡可以看出門限與容量的關係,永遠滿足loadFactor這個裝載因子
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //更新門限到threshold field
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //好吧,找它一圈終於找到table的初始化了
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //這裡就是當原始table不為空時,要有一個搬家的過程,所以這裡是最浪費效率的
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //先釋放,解脫它,而不是等著JVM自己收集,因為有可能導致根本沒有被收集,因為原始引用還在
                    oldTab[j] = null;
                    //當該桶位連結串列長度為1時,
                    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;
                        //這裡又是一個理解不太清楚的地方了
                           //(e.hash & oldCap) == 0,應該是表示原hash值小於oldcap,則其桶位不變,連結串列還 是在原位置
                        //若>0,則表示原hash值大於該oldCap,則桶位變為j + oldCap
                        //從結果來看等效於e.hash & (newCap - 1),只是不知道為何這樣計算
                        //而且與jdk1.6比,這裡連結串列並沒有被倒置,而jdk1.6中,每次擴容連結串列都會被倒置
                        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;
    }

      其實HashMap put方法中最重要的就是putVal和resize()這兩個方法了,搞懂他們,就基本搞懂HashMap的儲存方式了,因為get方法就是一個反向的過程。

      putAll方法就easy了

public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

3.get方法

      get方法同樣也是最常用的方法(不可能光存不取啊),可以看到通過getNode方法獲得節點後,直接取出Node的value屬性即可。

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;
        //這裡就是hash演算法查詢的過程,可以看到與put方法是一致的
        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);
            }
        }
        //值得注意的是當查詢不到的時候是返回null的
        return null;
    }

      相比於插入由於少了擴容的部分,get查詢簡化了很多,這裡也能看到hash演算法的精髓,快速定位查詢功能,不需要遍歷就能查詢到。

//containsKey,查詢是否存在某個鍵,getNode()大法好
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

4.remove方法

//根據鍵,刪除某一個節點,返回value值
 public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
/**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //這裡的node就是 所查詢到的節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                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儲存的是父節點,因為這裡涉及到連結串列刪除的操作
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //這裡可以看出matchValue這個引數的作用了
            //當matchValue為false時(即這裡我們remove方法所用的),直接短路後面的運算,進行刪除操作,而不用關注value值是否相等或者equals
            //而matchValue為true時,則只有在value值也符合時,才刪除
            //而jdk1.8也是給了過載方法,應用於當key鍵與value值同時匹配時,才進行刪除操作
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    //movable這個引數竟然是應用在了樹刪除上,可以再看一看
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //表示該節點就是連結串列的頭節點,則將子節點放進桶位
                else if (node == p)
                    tab[index] = node.next;
                //刪除節點後節點,父節點的next重新連線
                else