深入理解HashMap原理(一)——HashMap源碼解析

      轉載:http://blog.csdn.net/u013132758。 https://bthvi-leiqi.blog.csdn.net/article/details/89181005

介紹

HashMap原理是JAVA和Android面試中經常會遇到的問題,這篇文章將通過HashMap在JDK1.7和1.8 中的源碼來解析HashMap的原理。

相關概念

數組

採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

線性鏈表

對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

紅黑樹

紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組。
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。在二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
性質1. 節點是紅色或黑色。
性質2. 根節點是黑色。
性質3. 每個葉節點(NIL節點,空節點)是黑色的。
性質4. 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
性質5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

哈希表

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。

哈希衝突

如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。前面我們提到過,哈希函數的設計至關重要,好的哈希函數會儘可能地保證 計算簡單和散列地址分佈均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap即是採用了鏈地址法,也就是數組+鏈表的方式。

HashMap在JDK1.8中的源碼

首先我們看下源碼中的註釋:

//1、哈希表基於map接口的實現,這個實現提供了map所有的操作,並且提供了key和value可以爲null,(HashMap和HashTable大致上市一樣的除了hashmap是異步的和允許key和value爲null),
這個類不確定map中元素的位置,特別要提的是,這個類也不確定元素的位置隨着時間會不會保持不變。
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. 
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
 in particular, it does not guarantee that the order will remain constant over time. 

//假設哈希函數將元素合適的分到了每個桶(其實就是指的數組中位置上的鏈表)中,則這個實現爲基本的操作(get、put)提供了穩定的性能,迭代這個集合視圖需要的時間跟hashMap實例(key-value映射的數量)的容量(在桶中)
成正比,因此,如果迭代的性能很重要的話,就不要將初始容量設置的太高或者loadfactor設置的太低,【這裏的桶,相當於在數組中每個位置上放一個桶裝元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
 Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings
). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

//HashMap的實例有兩個參數影響性能,初始化容量(initialCapacity)和loadFactor加載因子,在哈希表中這個容量是桶的數量【也就是數組的長度】,一個初始化容量僅僅是在哈希表被創建時容量,在
容量自動增長之前加載因子是衡量哈希表被允許達到的多少的。當entry的數量在哈希表中超過了加載因子乘以當前的容量,那麼哈希表被修改(內部的數據結構會被重新建立)所以哈希表有大約兩倍的桶的數量
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, 
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
 its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table 
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

//通常來講,默認的加載因子(0.75)能夠在時間和空間上提供一個好的平衡,更高的值會減少空間上的開支但是會增加查詢花費的時間(體現在HashMap類中get、put方法上),當設置初始化容量時,應該考慮到map中會存放
entry的數量和加載因子,以便最少次數的進行rehash操作,如果初始容量大於最大條目數除以加載因子,則不會發生 rehash 操作。

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
 cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken 
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
 entries divided by the load factor, no rehash operations will ever occur.

//如果很多映射關係要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量創建它將使得映射關係能更有效地存儲。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting 
it perform automatic rehashing as needed to grow the table

HashMap的繼承關係

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    /****省略代碼****/
    }

我們看到HashMap繼承自AbstractMap實現了Map,Cloneable,Serializable接口。

HashMap的屬性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    //序列號,序列化的時候使用。
    private static final long serialVersionUID = 362498820763181265L;
    /**默認容量,1向左移位4個,00000001變成00010000,也就是2的4次方爲16,使用移位是因爲移位是計算機基礎運算,效率比加減乘除快。**/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量,2的30次方。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //加載因子,用於擴容使用。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //當某個桶節點數量大於8時,會轉換爲紅黑樹。
    static final int TREEIFY_THRESHOLD = 8;
    //當某個桶節點數量小於6時,會轉換爲鏈表,前提是它當前是紅黑樹結構。
    static final int UNTREEIFY_THRESHOLD = 6;
    //當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存儲元素的數組,transient關鍵字表示該屬性不能被序列化
    transient Node<K,V>[] table;
    //將數據轉換成set的另一種存儲形式,這個變量主要用於迭代功能。
    transient Set<Map.Entry<K,V>> entrySet;
    //元素數量
    transient int size;
    //統計該map修改的次數
    transient int modCount;
    //臨界值,也就是元素數量達到臨界值時,會進行擴容。
    int threshold;
    //也是加載因子,只不過這個是變量。
    final float loadFactor;  
    
    /****省略代碼****/
    
    }

這裏有一點就是默認爲什麼容量大小爲16,加載因子爲0.75.我們通過註釋來看:
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
大致意思就是 16和0.75是經過大量計算得出的最優解,當設置默認的大小和加載因子時,進行的rehhash此書後最少,性能上最優。

HashMap的構造方法

在這裏插入圖片描述
我們看到HashMap的構造方法有四個,
第一個:空參構造方法,使用默認的負載因子爲0.75;
第二個:設置初始容量並使用默認加載因子;
第三個:設置容量和加載因子,第二個構造方法最終還是調用了第三個構造方法;
第四個:將一個Map轉換爲HashMap。
下面我們看下第四個構造方法的源碼:

    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) {
        //獲取該map的實際長度
        int s = m.size();
        if (s > 0) {
            //判斷table是否初始化,如果沒有初始化
            if (table == null) { // pre-size
                /**求出需要的容量,因爲實際使用的長度=容量*0.75得來的,+1是因爲小數相除,基本都不會是整數,容量大小不能爲小數的,後面轉換爲int,多餘的小數就要被丟掉,所以+1,例如,map實際長度22,22/0.75=29.3,所需要的容量肯定爲30,有人會問如果剛剛好除得整數呢,除得整數的話,容量大小多1也沒什麼影響**/
                float ft = ((float)s / loadFactor) + 1.0F;
                //判斷該容量大小是否超出上限。
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                /**對臨界值進行初始化,tableSizeFor(t)這個方法會返回大於t值的,且離其最近的2次冪,例如t爲29,則返回的值是32**/
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果table已經初始化,則進行擴容操作,resize()就是擴容。
            else if (s > threshold)
                resize();
            //遍歷,把map中的數據轉到hashMap中。
            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);
            }
        }
    }

這裏我們看到構造函數中傳入了一個Map,然後把該Map轉換爲hashMap,這裏面還調用了resize()進行擴容,下面我們會詳細介紹。在上面的entrySet方法會返回一個Set<Map.Entry<K,V>>,泛型爲Map的內部類Entry,它是一個存放key-value的實例,爲什麼要用這種結構就是上面我們說的hash表的遍歷,插入效率高。構造函數基本已經講完了,下面我們重點看下HashMap是如何將key和value存儲的。下面我們看HashMap的put(K key,V value)方法.

HashMap的put方法

    public V put(K key, V value) {
        /**四個參數,第一個hash值,第四個參數表示如果該key存在值,如果爲null的話,則插入新的value,最後一個參數,在hashMap中沒有用,可以不用管,使用默認的即可**/
        return putVal(hash(key), key, value, false, true);
    }

我們看到這裏調用了putVal之前調用了hash方法;

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

我們看到這裏是將鍵值的hashCode做了異或運算,至於爲什麼這麼複雜,目的大致就是爲了減少哈希衝突。
下面我們看看putVal方法的源碼:

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       //tab 哈希數組,p 該哈希桶的首節點,n hashMap的長度,i 計算出的數組下標
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       //獲取長度並進行擴容,使用的是懶加載,table一開始是沒有加載的,等put後纔開始加載
       if ((tab = table) == null || (n = tab.length) == 0)
           n = (tab = resize()).length;
       /**如果計算出的該哈希桶的位置沒有值,則把新插入的key-value放到此處,此處就算沒有插入成功,也就是發生哈希衝突時也會把哈希桶的首節點賦予p**/
       if ((p = tab[i = (n - 1) & hash]) == null)
           tab[i] = newNode(hash, key, value, null);
       //發生哈希衝突的幾種情況
       else {
           // e 臨時節點的作用, k 存放該當前節點的key 
           Node<K,V> e; K k;
           //第一種,插入的key-value的hash值,key都與當前節點的相等,e = p,則表示爲首節點
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           //第二種,hash值不等於首節點,判斷該p是否屬於紅黑樹的節點
           else if (p instanceof TreeNode)
               /**爲紅黑樹的節點,則在紅黑樹中進行添加,如果該節點已經存在,則返回該節點(不爲null),該值很重要,用來判斷put操作是否成功,如果添加成功返回null**/
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           //第三種,hash值不等於首節點,不爲紅黑樹的節點,則爲鏈表的節點
           else {
               //遍歷該鏈表
               for (int binCount = 0; ; ++binCount) {
                   //如果找到尾部,則表明添加的key-value沒有重複,在尾部進行添加
                   if ((e = p.next) == null) {
                       p.next = newNode(hash, key, value, null);
                       //判斷是否要轉換爲紅黑樹結構
                       if (binCount >= TREEIFY_THRESHOLD - 1) 
                           treeifyBin(tab, hash);
                       break;
                   }
                   //如果鏈表中有重複的key,e則爲當前重複的節點,結束循環
                   if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                       break;
                   p = e;
               }
           }
           //有重複的key,則用待插入值進行覆蓋,返回舊值。
           if (e != null) { 
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       //到了此步驟,則表明待插入的key-value是沒有key的重複,因爲插入成功e節點的值爲null
       //修改次數+1
       ++modCount;
       //實際長度+1,判斷是否大於臨界值,大於則擴容
       if (++size > threshold)
           resize();
       afterNodeInsertion(evict);
       //添加成功
       return null;
   }

可以看到這裏主要有以下幾步:
1、根據key計算出在數組中存儲的下標
2、根據使用的大小,判斷是否需要擴容。
3、根據數組下標判斷是否當前下標已存儲數據,如果沒有則直接插入。
4、如果存儲了則存在哈希衝突,判斷當前entry的key是否相等,如果相等則替換,否則判斷下一個節點是否爲空,爲空則直接插入,否則取下一節點重複上述步驟。
5、判斷鏈表長度是否大於8當達到8時轉換爲紅黑樹。
下面我們看下HashMap的擴容函數resize()

HashMap的擴容函數resize()

    final Node<K,V>[] resize() {
        //把沒插入之前的哈希數組做我誒oldTal
        Node<K,V>[] oldTab = table;
        //old的長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //old的臨界值
        int oldThr = threshold;
        //初始化new的長度和臨界值
        int newCap, newThr = 0;
        //oldCap > 0也就是說不是首次初始化,因爲hashMap用的是懶加載
        if (oldCap > 0) {
            //大於最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                //臨界值爲整數的最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //標記##,其它情況,擴容兩倍,並且擴容後的長度要小於最大值,old長度也要大於16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //臨界值也擴容爲old的臨界值2倍
                newThr = oldThr << 1; 
        }
        /**如果oldCap<0,但是已經初始化了,像把元素刪除完之後的情況,那麼它的臨界值肯定還存在,        
           如果是首次初始化,它的臨界值則爲0
        **/
        else if (oldThr > 0) 
            newCap = oldThr;
        //首次初始化,給與默認的值
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            //臨界值等於容量*加載因子
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //此處的if爲上面標記##的補充,也就是初始化時容量小於默認值16的,此時newThr沒有賦值
        if (newThr == 0) {
            //new的臨界值
            float ft = (float)newCap * loadFactor;
            //判斷是否new容量是否大於最大值,臨界值是否大於最大值
            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;
        //此處自然是把old中的元素,遍歷到new中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //臨時變量
                Node<K,V> e;
                //當前哈希桶的位置值不爲null,也就是數組下標處有值,因爲有值表示可能會發生衝突
                if ((e = oldTab[j]) != null) {
                    //把已經賦值之後的變量置位null,當然是爲了好回收,釋放內存
                    oldTab[j] = null;
                    //如果下標處的節點沒有下一個元素
                    if (e.next == null)
                        //把該變量的值存入newCap中,e.hash & (newCap - 1)並不等於j
                        newTab[e.hash & (newCap - 1)] = e;
                    //該節點爲紅黑樹結構,也就是存在哈希衝突,該哈希桶中有多個元素
                    else if (e instanceof TreeNode)
                        //✨✨✨把此樹進行轉移到newCap中✨✨✨
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { /**此處表示爲鏈表結構,同樣把鏈表轉移到newCap中,就是把鏈表遍歷後,把值轉過去,在置位null**/
                        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) {
                                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;
                        }
                    }
                }
            }
        }
        //返回擴容後的hashMap
        return newTab;
    }

前面主要介紹了, HashMap的結構爲數組+ 鏈表(紅黑樹)。
總結一下上面的邏輯就是:
1、對數組進行擴容,
2、擴容後重新計算hashCode也就是key的下標,將原數據塞到新擴容後的數據結構中。
3、當存在hash衝突時,在數組後面以鏈表的形式追加到後面,當鏈表長度達到8時,就會將鏈表轉換爲紅黑樹。
那麼對於紅黑樹新增一個節點 ,我們考慮到前面所說的紅黑樹的性質。就需要對紅黑樹做調整,是紅黑樹達到平衡。這種平衡就是紅黑樹的旋轉。下面我們看看紅黑樹的旋轉:

紅黑樹的旋轉

紅黑樹的旋轉分爲左旋和右旋,以某個節點爲圓心向左或向右旋轉,具體我們通過下面的圖來看下[https://www.cnblogs.com/CarpenterLee/p/5503882.html]。

左旋

在這裏插入圖片描述
在這裏插入圖片描述

HashMap中紅黑樹的左旋


        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            TreeNode<K,V> r, pp, rl;
            if (p != null && (r = p.right) != null) {
                if ((rl = p.right = r.left) != null)
                    rl.parent = p;
                if ((pp = r.parent = p.parent) == null)
                    (root = r).red = false;
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
                r.left = p;
                p.parent = r;
            }
            return root;
        }

右旋

在這裏插入圖片描述
在這裏插入圖片描述

HashMap中紅黑樹的右旋

     static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
            TreeNode<K,V> l, pp, lr;
            if (p != null && (l = p.left) != null) {
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                if ((pp = l.parent = p.parent) == null)
                    (root = l).red = false;
                else if (pp.right == p)
                    pp.right = l;
                else
                    pp.left = l;
                l.right = p;
                p.parent = l;
            }
            return root;
        }

紅黑樹新增節點的例子

TreeMap的結構也是紅黑樹,它新增節點的過程如下:這裏跟HashMap的紅黑樹的新增原理一樣
在這裏插入圖片描述
我們通過這個例子有差不多已經瞭解了紅黑樹的原理。我們回到 resize()方法,裏面我們看
//✨✨✨把此樹進行轉移到newCap中✨✨✨
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

HashMap中TreeNode.split

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;	// 拿到調用此方法的節點
    TreeNode<K,V> loHead = null, loTail = null; // 存儲跟原索引位置相同的節點
    TreeNode<K,V> hiHead = null, hiTail = null; // 存儲索引位置爲:原索引+oldCap的節點
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {	// 從b節點開始遍歷
        next = (TreeNode<K,V>)e.next;   // next賦值爲e的下個節點
        e.next = null;  // 同時將老表的節點設置爲空,以便垃圾收集器回收
        //如果e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置一樣
        if ((e.hash & bit) == 0) {  
            if ((e.prev = loTail) == null)  // 如果loTail爲空, 代表該節點爲第一個節點
                loHead = e; // 則將loHead賦值爲第一個節點
            else
                loTail.next = e;    // 否則將節點添加在loTail後面
            loTail = e; // 並將loTail賦值爲新增的節點
            ++lc;   // 統計原索引位置的節點個數
        }
        //如果e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
        else {  
            if ((e.prev = hiTail) == null)  // 如果hiHead爲空, 代表該節點爲第一個節點
                hiHead = e; // 則將hiHead賦值爲第一個節點
            else
                hiTail.next = e;    // 否則將節點添加在hiTail後面
            hiTail = e; // 並將hiTail賦值爲新增的節點
            ++hc;   // 統計索引位置爲原索引+oldCap的節點個數
        }
    }
 
    if (loHead != null) {   // 原索引位置的節點不爲空
        if (lc <= UNTREEIFY_THRESHOLD)  // 節點個數少於6個則將紅黑樹轉爲鏈表結構
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;    // 將原索引位置的節點設置爲對應的頭結點
            // hiHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
            // 已經被改變, 需要重新構建新的紅黑樹
            if (hiHead != null) 
                loHead.treeify(tab);    // 以loHead爲根結點, 構建新的紅黑樹
        }
    }
    if (hiHead != null) {   // 索引位置爲原索引+oldCap的節點不爲空
        if (hc <= UNTREEIFY_THRESHOLD)  // 節點個數少於6個則將紅黑樹轉爲鏈表結構
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;  // 將索引位置爲原索引+oldCap的節點設置爲對應的頭結點
            // loHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
            // 已經被改變, 需要重新構建新的紅黑樹
            if (loHead != null) 
                hiHead.treeify(tab);    // 以hiHead爲根結點, 構建新的紅黑樹
        }
    }
}

這個方法中我們重點看treeify

HashMap中treeify

      final void treeify(Node<K,V>[] tab) {   // 構建紅黑樹
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即爲調用此方法的TreeNode
        next = (TreeNode<K,V>)x.next;   // next賦值爲x的下個節點
        x.left = x.right = null;    // 將x的左右節點設置爲空
        if (root == null) { // 如果還沒有根結點, 則將x設置爲根結點
            x.parent = null;    // 根結點沒有父節點
            x.red = false;  // 根結點必須爲黑色
            root = x;   // 將x設置爲根結點
        }
        else {
            K k = x.key;	// k賦值爲x的key
            int h = x.hash;	// h賦值爲x的hash值
            Class<?> kc = null;
            // 如果當前節點x不是根結點, 則從根節點開始查找屬於該節點的位置
            for (TreeNode<K,V> p = root;;) {	
                int dir, ph;
                K pk = p.key;   
                if ((ph = p.hash) > h)  // 如果x節點的hash值小於p節點的hash值
                    dir = -1;   // 則將dir賦值爲-1, 代表向p的左邊查找
                else if (ph < h)    // 與上面相反, 如果x節點的hash值大於p節點的hash值
                    dir = 1;    // 則將dir賦值爲1, 代表向p的右邊查找
                // 走到這代表x的hash值和p的hash值相等,則比較key值
                else if ((kc == null && // 如果k沒有實現Comparable接口 或者 x節點的key和p節點的key相等
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                	// 使用定義的一套規則來比較x節點和p節點的大小,用來決定向左還是向右查找
                    dir = tieBreakOrder(k, pk); 
 
                TreeNode<K,V> xp = p;   // xp賦值爲x的父節點,中間變量用於下面給x的父節點賦值
                // dir<=0則向p左邊查找,否則向p右邊查找,如果爲null,則代表該位置即爲x的目標位置
                if ((p = (dir <= 0) ? p.left : p.right) == null) { 
                    x.parent = xp;  // x的父節點即爲最後一次遍歷的p節點
                    if (dir <= 0)   // 如果時dir <= 0, 則代表x節點爲父節點的左節點
                        xp.left = x;
                    else    // 如果時dir > 0, 則代表x節點爲父節點的右節點
                        xp.right = x;
                    // 進行紅黑樹的插入平衡(通過左旋、右旋和改變節點顏色來保證當前樹符合紅黑樹的要求)
                    root = balanceInsertion(root, x);   
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root); // 如果root節點不在table索引位置的頭結點, 則將其調整爲頭結點
}

我們重點看這個方法balanceInsertion(root, x)這個方法就是使紅黑樹達到平衡。我們接着繼續看,要平衡紅黑樹就得左右旋轉。

HashMap中balanceInsertion

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            x.red = true;
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                if (xp == (xppl = xpp.left)) {
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.right) {
                            root = rotateLeft(root, x = xp);//對紅黑樹進行左旋
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);//對紅黑樹進行右旋
                            }
                        }
                    }
                }
                else {
                    if (xppl != null && xppl.red) {
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);//對紅黑樹進行右旋
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);//對紅黑樹進行左旋
                            }
                        }
                    }
                }
            }
       }

看到這裏基本思想已經明白了,我們下面總結一下:

總結

HashMap 的存儲結構

我們通過下面一副圖來看,數組+鏈表+紅黑樹
在這裏插入圖片描述

HashMap的擴容

我們通過下面的圖來看看HashMap的擴容過程
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
以上就是本文主要講解的HashMap 的核心思想,如有不對請指證。下一篇文章將帶大家手擼HashMap。

參考

1、史上最清晰的紅黑樹講解
2、Java集合:HashMap詳解(JDK 1.8)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章