1. 程式人生 > >HashMap的幾個構造方法解析

HashMap的幾個構造方法解析

HashMap的幾個構造方法原始碼解析(基於JDK1.8中的HashMap原始碼)

1、無參構造方法HashMap()

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
*/
public HashMap() {    //無參構造器
        //負載因子為預設值 0.75f
        //容量為預設初始值 16
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2、有一個初始容量引數的構造方法HashMap(int initialCapacity)

      引數:initialCapacity  初始容量

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        //此處通過把第二個引數負載因子使用預設值0.75f,然後呼叫有兩個引數的構造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

       這個一個引數的構造方法,使用HashMap的預設負載因子,把該初始容量和預設負載因子作為入參,呼叫HashMap的兩個引數的構造方法

3、有兩個引數的構造方法HashMap(int initialCapacity, float loadFactor)

引數:initialCapacity 初始容量

引數:loadFactor  負載因子

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     * 通過指定的初始容量和負載因子初始化一個空的HashMap
     *
     * @param  initialCapacity the initial capacity   初始化容量
     * @param  loadFactor      the load factor        負載因子
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     * 如果初始容量或者負載因子為負數,則會丟擲非法資料異常
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)   //如果初始容量小於0,丟擲異常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)  //如果初始容量超過最大容量(1<<32)
            initialCapacity = MAXIMUM_CAPACITY;  //則使用最大容量作為初始容量
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  //如果負載因子小於等於0或者不是數字,則丟擲異常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;                //把負載因子賦值給成員變數loadFactor

//呼叫tableSizeFor方法計算出不小於initialCapacity的最小的2的冪的結果,並賦給成員變數threshold
        this.threshold = tableSizeFor(initialCapacity); 
    }

我們下面看看tableSizeFor()這個方法是如何計算的,這個方法的實現原理很巧妙,原始碼如下:

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;      //容量減1,為了防止初始化容量已經是2的冪的情況,最後有+1運算。
        n |= n >>> 1;         //將n無符號右移一位再與n做或操作
        n |= n >>> 2;         //將n無符號右移兩位再與n做或操作
        n |= n >>> 4;         //將n無符號右移四位再與n做或操作
        n |= n >>> 8;         //將n無符號右移八位再與n做或操作
        n |= n >>> 16;        //將n無符號右移十六位再與n做或操作
        //如果入參cap為小於或等於0的數,那麼經過cap-1之後n為負數,n經過無符號右移和或操作後仍未負 
        //數,所以如果n<0,則返回1;如果n大於或等於最大容量,則返回最大容量;否則返回n+1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

借用一位博主對tableSizeFor()方法的梳理:該演算法分析原文地址(https://blog.csdn.net/fan2012huan/article/details/51097331)

首先,為什麼要對cap做減1操作。int n = cap - 1; 
這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個cap的2倍。如果不懂,要看完後面的幾個無符號右移之後再回來看看。 
下面看看這幾個無符號右移操作: 
如果n這時為0了(經過了cap-1之後),則經過後面的幾次無符號右移依然是0,最後返回的capacity是1(最後有個n+1的操作)。 
這裡只討論n不等於0的情況。 
第一次右移

n |= n >> 1;

由於n不等於0,則n的二進位制表示中總會有一bit為1,這時考慮最高位的1。通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進位制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。 
第二次右移

n |= n >>> 2;

注意,這個n已經經過了n |= n >>> 1; 操作。假設此時n為000011xxxxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,然後再與原來的n做或操作,這樣n的二進位制表示的高位中會有4個連續的1。如00001111xxxxxx 。 
第三次右移

n |= n >>> 4;

這次把已經有的高位中的連續的4個1,右移4位,再做或操作,這樣n的二進位制表示的高位中會有8個連續的1。如00001111 1111xxxxxx 。 
以此類推 
注意,容量最大也就是32bit的正數,因此最後n |= n >>> 16; ,最多也就32個1,但是這時已經大於了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。 
舉一個例子說明下吧。 


這個演算法著實牛逼啊!

注意,得到的這個capacity卻被賦值給了threshold。

this.threshold = tableSizeFor(initialCapacity);
開始以為這個是個Bug,感覺應該這麼寫:

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。 
但是,請注意,在構造方法中,並沒有對table這個成員變數進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold重新計算。
 

4、有一個Map型別的引數的構造方法

/**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     * 根據傳入的指定的Map引數去初始化一個新的HashMap,該HashMap擁有著和原Map中相同的對映關係
     *  以及預設的負載因子(0.75f)和一個大小充足的初始容量
     * @param   m the map whose mappings are to be placed in this map
     * 引數 m  一個對映關係將會被新的HashMap所取代的Map
     * @throws  NullPointerException if the specified map is null
     * 如果這個Map為空的話,將會丟擲空指標異常
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;      //將預設的負載因子賦值給成員變數loadFactor
        putMapEntries(m, false);                    //呼叫PutMapEntries()來完成HashMap的初始化賦值過程
    }

我們看下putMapEntries()方法,這個方法呼叫了HashMap的resize()擴容方法和putVal()存入資料方法,原始碼如下:

/**
     * Implements Map.putAll and Map constructor
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();        //定義一個s,大小等於map的大小,這個未做非空判斷,可能丟擲空指標異常
        if (s > 0) {                 //如果map鍵值對個數大於0
            if (table == null) {     // pre-size   如果當前的HashMap的table為空
                float ft = ((float)s / loadFactor) + 1.0F;   //計算HashMap的最小需要的容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?    //如果該容量大於最大容量,則使用最大容量
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)        //如果容量大於threshold,則對對容量計算,取大於該容量的最小的2的冪的值
                                        //並賦給threshold,作為HashMap的容量。tableSizeFor()方法講解在上面
                                        //已經有了說明,不明白的小夥伴可以往上翻翻看。
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold) //如果table不為空,即HashMap中已經有了資料,判斷Map的大小是否超過了HashMap的閾值
                resize();                //如果超過閾值,則需要對HashMap進行擴容
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {  //對Map的EntrySet進行遍歷
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);        //呼叫HashMap的put方法的具體實現方法來對資料進行存放。
            }
        }
    }

要看懂putMapEntries()方法,就必須弄懂resize()方法和putVal()方法,下面我們對這兩個方法原始碼進行分析。

我們下面看看resize方法的原始碼:

/**
     * 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.
     * 初始化或把table容量翻倍。如果table是空,則根據threshold屬性的值去初始化HashMap的容                
     * 量。如果不為空,則進行擴容,因為我們使用2的次冪來給HashMap進行擴容,所以每個箱子裡的元素 
     * 必須保持在原來的位置或在新的table中以2的次冪作為偏移量進行移動
     *
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //定義一個oldTab存放當前的table
        Node<K,V>[] oldTab = table;
        //判斷當前的table是否為空,如果為空,則把0賦值給新定義的oldCap,否則以table的長度作為oldCap的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;                //把table的閾值賦值給oldThr變數
        int newCap, newThr = 0;                //定義變數newCap和newThr來存放新的table的容量和閾值
        if (oldCap > 0) {                            //如果原來的table長度大於0
            if (oldCap >= MAXIMUM_CAPACITY) {        //判斷長度是否大於HashMap的最大容量
                threshold = Integer.MAX_VALUE;        //以int的最大值作為原來HashMap的閾值,並把原來的table返回
                return oldTab;
            }
//如果原table容量不超過HashMap的最大容量,將 原容量*2 賦值給變數newCap,如果newCap不大於HashMap的最大容量,並且原容量大於HashMap的預設容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //將newThr的值設定為 原HashMap的閾值*2
                newThr = oldThr << 1; // double threshold
        }
        //如果原容量不大於0,即原table為null,並且原閾值大於0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //將原閾值作為容量賦值給newCap當做newCap的值
            newCap = oldThr;
        // 如果原容量不大於0,別切原閾值也不大於0
        else {               // zero initial threshold signifies using defaults
            //則以預設容量作為newCap的值
            newCap = DEFAULT_INITIAL_CAPACITY;
            //以初始容量*預設負載因子的結果作為newThr值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //經過上面的處理過程,如果newThr值為0,給newThr進行賦值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //將新的閾值newThr賦值給threshold,為新初始化的HashMap來使用
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            //初始化一個新的容量大小為newCap的Node陣列
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //給table重新賦值
        table = newTab;
        if (oldTab != null) {        //如果原來的HashMap中有值,則遍歷
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {    //如果原來的table陣列中第j個位置不為空
                    oldTab[j] = null;             //把e = oldTab[j],然後讓oldTab[j]置空
                    if (e.next == null)           //如果e.next = null,說明e.next不存在其他Node
                        newTab[e.hash & (newCap - 1)] = e; //此時以e.hash&(newCap-1)的結果作為e在newTab中的位置
                    else if (e instanceof TreeNode)  //否則判斷e的型別是TreeNode還是Node,即連結串列和紅黑樹判斷
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果時紅黑樹,則進行紅黑樹的處理
                    else { // preserve order    //如果是連結串列
                        //定義了五個Node變數,我一直想知道lo和hi是是哪兩個單詞的縮寫,
                        //根據程式碼來看應該是lower和higher吧,也就是高位和低位,
                        //因為我們知道HashMap擴容時,容量會擴到原容量的2倍,
                        //也就是放在連結串列中的Node的位置可能保持不變或位置變成 原位置+oldCap ,
                        //這裡的高低應該就是這個意思吧,當然這只是個人理解。
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {                    //迴圈連結串列中的Node
                            next = e.next;
                            //如果e.hash & oldCap == 0,注意這裡是oldCap,而不是oldCap-1。
                            //我們知道oldCap是2的次冪,也就是1、2、4、8、16...轉化為二進位制之後,
                            //都是高位為1,其它位為0。所以oldCap & e.hash 也是隻有e.hash值在oldCap二進位制不為0的位對應的位也不為0時,
                            //才會得到一個不為0的結果。舉個例子,我們知道10010 和00010 與1111的&運算結果都是 0010  ,
                            //但是110010和010010與10000的運算結果是不一樣的,所以HashMap就是利用這一點,
                            //來判斷當前在連結串列中的資料,在擴容時位置時保持不變還是位置移動oldCap。
                            if ((e.hash & oldCap) == 0) {     //如果結果為0,即位置保持不變   
                                if (loTail == null)            //如果是第一次遍歷
                                    loHead = e;                //讓loHead = e
                                else
                                    loTail.next = e;            //否則,讓loTail的next = e
                                loTail = e;                     //最後讓loTail = e
                            }
                            //其實if 和else 中做的事情是一樣的,我們看到有loHead和loTail兩個Node,
                            //我們其實可以把loHead當做頭元素,然後loTail是用來維護loHead的,即每次迴圈,
                            //更新loHead的next。我們來舉個例子,比如原來的連結串列是A->B->C->D->E。
                            //我們這裡把->假設成next關係,這五個Node中,只有C的hash & oldCap != 0 ,
                            //然後這個程式碼執行過程就是:
//第一次迴圈:      先拿到A,把A賦給loHead,然後loTail也是A
//第二次迴圈:      此時e的為B,而且loTail != null,也就是進入上面的else分支,把loTail.next =                     
//                 B,此時loTail中即A->B,同樣反應在loHead中也是A->B,然後把loTail = B
//第三次迴圈:      此時e = C,由於C不滿足 (e.hash & oldCap) == 0,進入到了我們下面的else分支,其 
//                 實做的事情和當前分支的意思一樣,只不過維護的是hiHead和hiTail。
//第四次迴圈:      此時e的為D,loTail != null,進入上面的else分支,把loTail.next =                     
//                 D,此時loTail中即B->D,同樣反應在loHead中也是A->B->D,然後把loTail = D
//.
//.
//.             
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //遍歷結束,即把table[j]中所有的Node處理完
                        if (loTail != null) {        //如果loTail不為空,此時保證了loHead不為空
                            loTail.next = null;      //此時把loTail的next置空
                            newTab[j] = loHead;      //把loHead放在newTab陣列的第j個位置上
                        }
                        if (hiTail != null) {        同理,只不過hiHead放的位置是j+oldCap
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;            //最後返回newTab
    }

我們上面講了resize()方法,下面看看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;
        if ((tab = table) == null || (n = tab.length) == 0)            //如果hashMap為空,則使用resize方法去初始化HashMap
            n = (tab = resize()).length;            //把table陣列的長度賦值給n
        if ((p = tab[i = (n - 1) & hash]) == null)    //如果tab的第(n-1) & hash為空,即此處沒有Node
            tab[i] = newNode(hash, key, value, null); //則初始化一個新的Node存放在此處
        else {
            Node<K,V> e; K k;                         //如果需要存放的位置已經存在了鍵值對
            if (p.hash == hash &&                    //判斷此處鍵值對的key是否和我們要存入的鍵值對的key相同
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;            //如果相同,則把此處的Node賦給Node 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) { //如果當前節點的next為空,即當前連結串列中不存在我們要存放的鍵值對
                        p.next = newNode(hash, key, value, null); //則把當前節點的next賦值為我們要存放的鍵值對
                        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)))) //如果連結串列中存在鍵值對的key和我們要存入的鍵值對的key相同的Node,則跳出迴圈,此時e = p.next
                        break;
                    p = e;  //把p = p.next 進行遍歷
                }
            }
            if (e != null) { // existing mapping for key  //如果e不為空,此處的e為我們要存放value的鍵值對
                V oldValue = e.value;                    //把原來的值取出來
                if (!onlyIfAbsent || oldValue == null)   //如果onlyIfAbsent為false或者oldValue為null則進行覆蓋,預設onlyIfAbsent為false
                    e.value = value;
                afterNodeAccess(e);                    //這個為linkedHashMap中才有意義,HashMap為空方法
                return oldValue;
            }
        }
        ++modCount;                                //讓HashMap的修改次數+1
        if (++size > threshold)                    //判斷當前Hash的鍵值對數量是否超過擴容閾值
            resize();                              //如果超過擴容閾值則進行擴容
        afterNodeInsertion(evict);    //這個為linkedHashMap中才有意義,HashMap為空方法
        return null;
    }

終於分析完了,自己也從中獲益很多,希望看到的同學也能從中獲益。2018-12-20 今天英雄聯盟德瑪西亞杯哦~