深刻理解HashMap原理(一)——HashMap源碼解析(JDK 1.8)

介紹

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

相關概念

數組

採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲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) 函數。app

哈希衝突

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

HashMap在JDK1.8中的源碼

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

//一、哈希表基於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);
   }

咱們看到這裏是將鍵值的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;
   }

能夠看到這裏主要有如下幾步:
一、根據key計算出在數組中存儲的下標
二、根據使用的大小,判斷是否須要擴容。
三、根據數組下標判斷是否當前下標已存儲數據,若是沒有則直接插入。
四、若是存儲了則存在哈希衝突,判斷當前entry的key是否相等,若是相等則替換,不然判斷下一個節點是否爲空,爲空則直接插入,不然取下一節點重複上述步驟。
五、判斷鏈表長度是否大於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的結構爲數組+ 鏈表(紅黑樹)。
總結一下上面的邏輯就是:
一、對數組進行擴容,
二、擴容後從新計算hashCode也就是key的下標,將原數據塞到新擴容後的數據結構中。
三、當存在hash衝突時,在數組後面以鏈表的形式追加到後面,當鏈表長度達到8時,就會將鏈表轉換爲紅黑樹。
那麼對於紅黑樹新增一個節點 ,咱們考慮到前面所說的紅黑樹的性質。就須要對紅黑樹作調整,是紅黑樹達到平衡。這種平衡就是紅黑樹的旋轉。下面咱們看看紅黑樹的旋轉:

紅黑樹的旋轉

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

左旋

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

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。[深刻理解HashMap原理(二)——手寫HashMap
](https://blog.csdn.net/u013132...

其餘文章

一、Gradle

gradle 詳解——你真的瞭解Gradle嗎?

一分鐘幫你提高Android studio 編譯速度

二、Flutter

Flutter從入門到實戰

三、源碼

深刻理解HashMap原理(一)——HashMap源碼解析(JDK 1.8)

深刻理解HashMap原理(二)——手寫HashMap

Handler 源碼解析——Handler的建立

四、熱修復

Android學習——手把手教你實現Android熱修復

Android熱修復——深刻剖析AndFix熱修復及本身動手實現

手擼一款Android屏幕適配SDK
Android自定義無壓縮加載超清大圖

參考

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

相關文章
相關標籤/搜索