1. 程式人生 > >HashMap原始碼解析(JDK8)

HashMap原始碼解析(JDK8)

前言

這段時間有空,專門填補了下基礎,把常用的ArrayListLinkedListHashMapLinkedHashMapLruCache原始碼看了一遍,List相對比較簡單就不單獨介紹了,Map準備用兩篇的篇幅,分別介紹HashMap和(LruCache+LinkedHashMap),因為LruCache是用LinkedHashMap實現的所以就和Lru一起介紹了。

概述

  • HashMap是一個用來儲存鍵值對的容器,並且key唯一value可以重複,執行緒不安全,遍歷時無序。
  • 底層是通過陣列實現稱之為雜湊桶,數組裡面裝的是單項鍊表
  • 雜湊桶的容量是2的次方
    ,這樣做的目的是為了計算插入位置的時候可以直接用位運算與替代取餘操作提高效率 。
  • 預設擴容方式為容量 * 2、閾值 * 2,新增元素時當連結串列長度>=8時會轉換為紅黑樹提高查詢效率,擴容時當紅黑樹中元素<=6時會轉回連結串列。擴容後元素的下標是根據hash與上舊的容量算出,如果==0則代表在低位下標不變,如果 != 0則代表在高位則為原下標+原容量。
  • 從迭代器可以看出迭代順序是無序的,按桶的下標從小到大,連結串列從前往後迭代。
  • key的雜湊值並不是僅僅通過hashCode()方法返回,還加上了擾動函式使hashcode的高位也能參與插入桶下標的計算減少雜湊衝突,因為hashCode()
    方法返回的是Int型的值而Int取值範圍是2的32次方與上(我們桶數-1)計算插入下標的方式,預設情況只有低位參與了運算,那麼即使hashCode()方法返回的值是唯一的但是由於只有低位參與運算大大的增大了碰撞的可能性,所以需要擾動函式處理下讓高位也參與進下標的計算來減少雜湊碰撞的可能性。

正文

接下來將按構造方法、增、刪、改、查、迭代的順序一一講解,看原始碼相對會比較枯燥,不過沒事我會加上大量的註釋幫助理解。接下來開始吧。

構造方法

	static final int MAXIMUM_CAPACITY = 1 << 30;//容量最大值
	transient Node<
K,V>
[] table;//雜湊桶 final float loadFactor;//載入因子 threshold = 雜湊桶.length * loadFactor int threshold;//閾值 當雜湊桶中元素數量超過閾值的時候會觸發resize()擴容 static final float DEFAULT_LOAD_FACTOR = 0.75f;//預設載入因子 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //預設容量16 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);//返回通過tableSizeFor方法處理的容量,這裡稍微有點歧義他把容量賦值給了threshold閾值,不過後面他會把這個閾值賦給容量然後重新計算閾值。 } //獲取新的容量,返回的值為最近接並且>=cap的2的n次方,方便後面用與運算代替取餘 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; } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);//呼叫第一個構造方法預設載入因子0.75 } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map<? extends K, ? extends V> m) {//傳入一個map存到我們新建立的map中 this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }

可以發現上面的建構函式主要功能就是初始化載入因子loadFactor和容量,一般情況下載入因子我們使用預設的0.75,接下來看第四個構造方法中的putMapEntries()方法

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();//拿到要新增的map的size
        if (s > 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);//獲取最接近的並且>=t的2的n的值作為容量
            }
            else if (s > threshold)//如果size大於threshold擴容
                resize();//擴容
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {//for迴圈將值一一插入
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);//put鍵值對到map
            }
        }
    }

這個方法中又出現了2個新的方法resize()擴容和putVal()增加,putVal()後面會講,這裡我們先看非常重要的擴容方法resize()

    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;//閾值改為Integer.MAX_VALUE,容量不變
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//新的容量為舊容量*2
                newThr = oldThr << 1; // 新的閾值為舊閾值*2
        }
        else if (oldThr > 0) //雜湊表未初始化,但是有閾值
            newCap = oldThr;// 這個就是我們前面說過的他在構造方法的時候把容量賦給閾值的情況,這裡他把前面計算得到的容量通過oldThr賦值給了新的newCap容量,後面他會重新計算閾值。
        else {//雜湊桶未初始化 容量也未初始化
            newCap = DEFAULT_INITIAL_CAPACITY;//預設容量16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//預設閾值12
        }
        if (newThr == 0) {//如果前面判斷走的else if即newThr為0重新計算閾值
            float ft = (float)newCap * loadFactor;//計算閾值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);//邊界判斷
        }
        /**
        * 上面這一大段其實就是計算新的容量和閾值,容量的預設值為16閾值預設值為12,預設擴容方式是*2。
        * 下面的話則是新建一個桶然後把原來的資料裝到新桶中
        */
        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) {//如果桶中元素不為null賦值給e
                    oldTab[j] = null;//去除舊的桶中的引用
                    if (e.next == null)//如果連結串列中節點沒有下一個元素則沒發生碰撞
                        newTab[e.hash & (newCap - 1)] = e;//直接把節點的hash與上新的容量-1得出下標裝入新桶中
                    else if (e instanceof TreeNode)//如果是樹節點則代表此處是紅黑樹 由於紅黑樹不是本篇重點這裡就略過了
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//將紅黑樹中的節點新增到新的桶中
                    else { //該節點是個連結串列
                        Node<K,V> loHead = null, loTail = null;//低位的頭和尾
                        Node<K,V> hiHead = null, hiTail = null;//高位的頭和尾
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//hash與上舊的容量如果==0則在低位,否則在高位
                                if (loTail == null)//如果尾部為null
                                    loHead = e;//新增到頭部
                                else
                                    loTail.next = e;//尾部下一個為e
                                loTail = e;//尾部為e
                            }
                            else {//位置在高位 完成連結串列的組裝
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);//如果下一個元素不為null
                        if (loTail != null) {//低位連結串列不為空
                            loTail.next = null;
                            newTab[j] = loHead;//新增到原始下標j
                        }
                        if (hiTail != null) {//高位連結串列不為空
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//新增到原始下標j+舊的容量
                        }
                    }
                }
            }
        }
        return newTab;
    }

構造方法和擴容方法resize()就說完了,簡單總結下。

  1. 構造方法就是對載入因子loadFactor和容量做了初始化,雖然構造方法中容量一開始是threshold變數儲存的有點奇怪不過後面,他會把threshold賦值給newCap並重新計算閾值所以沒有問題。

  2. 擴容方法resize()實現分為兩步

    1. 計算新的容量和閾值,預設容量16閾值12,然後擴容的方式是*2
    2. 建立新的桶,將原有的元素放到新的桶中,需要注意的是插入新桶的下標是根據雜湊值與上舊容量得出,低位的話下標不變,高位的話下標為原下標+原容量得出。

增、改

增和改都是同一個方法put這裡就一起講了

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

先看下獲取雜湊值的hash()方法

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

可以看到它是key的雜湊值異或了高位的值,這部分^ (h >>> 16)就是我們前面提到的擾動函式讓高位也參與下標的運算減少雜湊衝突的機率。

	static final int TREEIFY_THRESHOLD = 8;//連結串列轉為紅黑樹的界限
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i//宣告表變數tab,要插入的位置上的原始元素p,容量n,插入下標i
        if ((tab = table) == null || (n = tab.length) == 0)//如果表為空或者容量為0
            n = (tab = resize()).length;//初始化表
        if ((p = tab[i = (n - 1) & hash]) == null)//要插入位置上沒有元素即沒發生碰撞
            tab[i] = newNode(hash, key, value, null);//直接插入該位置
        else {//發生了碰撞
            Node<K,V> e; K k;//宣告節點變數e代表找到了與要插入元素key一樣節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//如果要插入元素key的hash值與該位置上元素相同,並且key相等。
                e = p;//將要插入位置上的原始元素p賦值給e
            else if (p instanceof TreeNode)//如果要插入位置上的原始元素是樹節點
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//找到了key的hash值相同,key也相等的元素賦值給e
            else {//要插入位置上是一個連結串列
                for (int binCount = 0; ; ++binCount) {//遍歷連結串列
                    if ((e = p.next) == null) {//如果下個元素為null
                        p.next = newNode(hash, key, value, null);//直接插入連結串列尾部
                        if (binCount >= TREEIFY_THRESHOLD - 1) //如果連結串列長度大於等於8
                            treeifyBin(tab, hash);//連結串列轉為紅黑樹
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//找到了key雜湊值相同並且相等的元素就停止遍歷
                        break;
                    p = e;
                }
            }
            if (e != null) { //存在key相同的
                V oldValue = e.value;//拿到舊的值
                if (!onlyIfAbsent || oldValue == null)//判斷是否允許覆蓋已有的鍵值對,預設可以覆蓋
                    e.value = value;//替換value的值
                afterNodeAccess(e);
                return oldValue;//返回舊的值
            }
        }
        ++modCount;//修改數++
        if (++size > threshold)//判斷size是否超過閾值
            resize();//擴容
        afterNodeInsertion(evict);
        return null;
    }

簡單總結下

  1. key的雜湊值除了通過hashCode()方法獲取,^ (h >>> 16)還異或了高位減少雜湊衝突。
  2. put元素的時候先判斷該位置是否有元素,沒有直接插入,有的話即雜湊衝突了,那麼比較key的雜湊值是否相同並且key是否相等,如果相同預設情況會替換value,如果不相同插入連結串列尾部或者紅黑樹,如果連結串列長度大於等於8的話會轉為紅黑樹,新增完成後再判斷size是否大於threshold閾值,如果大於則擴容。

    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) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;//宣告變數tab為雜湊表,p為要刪除下標的元素,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);//找到紅黑樹中key雜湊相同值相同的元素
                else {//為連結串列
                    do {//遍歷連結串列
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {//找到連結串列中key雜湊相同值相同的元素
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue ||