1. 程式人生 > >java集合中的HashMap源碼分析

java集合中的HashMap源碼分析

ble 第一個 我們 alt 構造方法 分析 ado raw mage

1.hashMap中的成員分析

   transient Node<K,V>[] table;    //為hash桶的數量
/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;      //hashMap中鍵值對的數量

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     
*/ transient int modCount; //用來記錄hashMap被改變的次數,進行fail-fast /** * The next size value at which to resize (capacity * load factor). * * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold; //用來表示HashMap的闕值 threshold = capacity*loadFactor /** * The load factor for the hash table. * * @serial */ final float loadFactor; //用來表示HashMap的加載因子

2.hashMap中的重要方法分析

  (1).hash方法(用來根據key來獲取hash值)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //註意當key為null是會返回0,hashmap允許key為null進行存儲,且存在table[0]的位置。
                                           另外會對獲取的hashcode進行高低16位按位與,減小hash沖突的概率
}

  (2).tableSizeFor(使用此方法來讓我們的容量變為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;
}

  (3).put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); //會去調用putVal方法
}
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)   //判斷table是否為null或table的大小是否為0
            n = (tab = resize()).length;             //如果上述條件成立,那麽調用resize()方法去擴容
        if ((p = tab[i = (n - 1) & hash]) == null)       //計算插入的元素在hash桶中的位置,若該位置還沒有元素
            tab[i] = newNode(hash, key, value, null);     //新建一個node節點,並將該節點添加到該位置
        else {                            //否則,執行以下代碼
            Node<K,V> e; K k;
            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);   
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  //判斷鏈表的長度是否大於8
                            treeifyBin(tab, hash);         //將鏈表轉為紅黑樹
                        break;                  
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))   //如果某個節點與我們即將插入的節點相同,則跳出循環
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key   //此時e節點中記錄的是hashMap中與我們要插入的節點相同的節點,在這裏統一進行處理
                V oldValue = e.value;              //記錄舊的value值
                if (!onlyIfAbsent || oldValue == null)     //通過onlyIfAbsent與oldValue的值判斷是否要進行覆蓋
                    e.value = value;              //覆蓋舊的值
                afterNodeAccess(e);              //此方法與LinkedHashMap相關,是一個回調方法,我們之後的文章再進行分析
                return oldValue;                //返回舊的值
            }
        }
        ++modCount;                      //代表hashMap的結構被改變
        if (++size > threshold)               //判斷是否要進行擴容
            resize();
        afterNodeInsertion(evict);              //此方法也是與LinkedHashMap相關的方法
        return null;
    }

  (4).resize(用來對hashMap進行擴容)

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;                //記錄原來的table
        int oldCap = (oldTab == null) ? 0 : oldTab.length;   //判斷原來的table是否為空,若為空則oldCap = 0,否則oldCap = oldTab.length
        int oldThr = threshold;                   //記錄原來的闕值
        int newCap, newThr = 0;                   //創建變量用來記錄新的容量和闕值
        if (oldCap > 0) {                      //判斷原來的容量是否大於0,由於HashMap是在第一次put是才會進行初始化,因此此方法也是判斷table是要擴容還是要初始化.大於0代表已經初始化過了
            if (oldCap >= MAXIMUM_CAPACITY) {          //如果原來的容量大於0且大於等於最大值
                threshold = Integer.MAX_VALUE;         //將闕值設為最大值,並返回原來的容量,代表該table已經不能再進行擴容
                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    //如果說oldCap為0(代表hashMap沒有被初始化過)且原來的闕值大於0(此處需要看一下hashmap的構造方法)
            newCap = oldThr;                              //將新的容量設置為原來的闕值
        else {               // zero initial threshold signifies using defaults//否則說明我們在新建hashMap是沒有指定初始值或是我們將初始大小設為了0
            newCap = DEFAULT_INITIAL_CAPACITY;                   //設為默認值16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  //闕值設為16*0.75
        }
        if (newThr == 0) {                              //如果新的闕值為0(此處表示在新建hashMap是給定了capacity且不為0)
            float ft = (float)newCap * loadFactor;                //設置為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數組(可能是初始化,也可能是擴容)
        table = newTab;                      
        if (oldTab != null) {                //若原來的table是否為空,代表現在是要進行擴容操作
            for (int j = 0; j < oldCap; ++j) {      //遍歷hash桶
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {      //遍歷每一個hash桶中的元素,並記錄第一個節點到變量e
                    oldTab[j] = null;           //將原來的位置設為null   
                    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;     //將每一個桶中的元素分為兩類,擴容後的位置與原來相同則記錄到loHead,loTail這個鏈表中,擴容後與原來的位置不同則記錄到hiHead,hiTail中
                        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;      //將loHead鏈表寫入到新的table
                        }
                        if (hiTail != null) {        //將hiHead鏈表記錄到新的table
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

3.hashMap中的一些細節問題

  (1).為什麽table的大小為2的倍數,且以2倍進行擴容?

    ① 在傳統的散列中我們一般使用奇數作為table的長度,由於奇數的因子只有1與本身,在進行取余操作時可以避免hash沖突的概率。但是當我們在進行resize操作時需要去一個個的去計算新的位置。

    ② 當我們以二倍擴容後,我們發現每次擴容後只是hashCode多長來了一位計算為,我們只需要去判斷多出來的計算位是1 or 0 就可以判斷新的位置不變還是在(oldCap+OldPos)的位置。

  (2).為什麽在鏈表節點大於8是轉換為紅黑樹?(參考源碼中的註釋)

    技術分享圖片

java集合中的HashMap源碼分析