【JDK1.8】一文看懂HashMap

HashMap集合簡介

HashMap 基於哈希表的Map接口實現,是以 key-value 存儲形式存在,即主要用來存放鍵值對。HashMap 的實現是不同步的,這意味着他不是線程安全的。他的 key、value 都可以爲 null。 此外,HashMap 中的映射不是有序的

JDK1.8之前 HashMap 由 數組+鏈表 組成,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突(兩個對象調用的 hashCode 方法計算的哈希碼值一致導致計算的數組索引值相同)。

JDK1.8之後,HashMap 由 數組+鏈表+紅黑樹 組成,當連表長度大於閾值(或者紅黑樹的邊界值,默認爲8)並且當前的數組長度大於64時,此時此索引位置上的所有數據改爲使用紅黑樹存儲。

補充:將鏈表轉換成紅黑樹前會判斷,即使閾值大於8但是數組長度小於64,此時並不會將鏈表變爲紅黑樹,而是進行數組擴容。

原因:數組長度小於64時,相對於轉換成紅黑樹,把數組擴容會快很多
在這裏插入圖片描述雖然JKD1.8新增加了紅黑樹作爲底層數據結構,結構變得更復雜了,但是閾值大於8並且數組長度大於64時,鏈表轉換爲紅黑樹,效率將會變得更高效

特點小結:
  1. HashMap是存取無序的

  2. 鍵和值位置都可以是null,但是鍵位置只能有一個是null

  3. 鍵位置是唯一的,底層的數據結構控制鍵

  4. JDK1.8之前是數組+鏈表,JDK1.8之後纔是數組+鏈表+紅黑樹

  5. 閾值(邊界值)大於8並且數組長度大於64,纔將鏈表轉換爲紅黑樹,變爲紅黑樹的目的是爲了高效的查詢

HashMap集合底層的數據結構

HashMap集合底層的數據結構存儲數據的過程

在這裏插入圖片描述相關面試題:

  1. Q: HashMap 中 hash 函數是怎麼實現的?還有哪些 hash 函數的實現方式?
    A:對於 key 的 hashCode 做 hash 操作,無符號右移16位然後做異或運算。除此之外,還可以用取餘數法、僞隨機數法等,但是這些效率都比較低,而無符號右移16位的異或運算是效率最高的

  2. Q:當兩個對象的hashCode相等時會怎麼樣?
    A:會發生哈希碰撞。若 key 值內容相同,則替換舊的 value,否則連接到鏈表後面
    鏈表長度超過8,數組長度超過64的時候,會將鏈表轉換爲紅黑樹

  3. Q:何時發生哈希碰撞?什麼是哈希碰撞?如何解決哈希碰撞?
    A:只要兩個元素的 key 計算的哈希值相同就會發生哈希碰撞
    jdk1.8之前使用鏈表解決哈希碰撞,jdk1.8之後使用鏈表+紅黑樹解決哈希碰撞

  4. Q:如果兩個鍵的hashCode相同,如何存儲鍵值對?
    A:hashCode 相同,通過 equal 方法比較內容是否相同
    相同:新的 value 覆蓋舊的 value,返回舊的 value
    不相同:將新的鍵值對添加到鏈表中

總結:

在這裏插入圖片描述
上圖的一些說明:

  1. size 表示 HashMap 中 K-V 的實時數量,不等於數組長度
  2. threshold(臨界值) = capacity(容量) * loadFactor(加載因子 0.75)。這個值是當前已佔用數組長度的最大值。size超過這個臨界值就會重新resize(擴容),擴容後的HashMap容量是之前容量的兩倍,在實際開發中,如果對效率的要求很高,應當儘量避免對 HashMap 的擴容

HashMap的繼承關係

在這裏插入圖片描述
說明:
Cloneable:空接口,表示可以克隆。創建並返回一個 HashMap 對象的一個副本
Serializable:序列化接口。HashMap 對象可以被序列化或反序列化
AbstractMap:父類提供了 Map 的實現接口,提供了一些基本的方法,以最大限度減少實現 Map 接口所需的工作

奇怪的現象:
爲什麼AbstractMap實現了Map接口,HashMap已經繼承了AbstractMap,爲什麼還要再實現一遍Map接口呢?
原因:
據 Java 集合框架的創始人 Josh Bloch 描述,這樣的寫法是一個失誤。在 Java 集合框架中,類似這樣的寫法有很多,最開始寫 Java 集合框架的時候,他認爲這樣寫,在某些地方可能是有價值的,直到他意識到錯了。顯然,JDK 的維護者認爲這個小小的失誤不值得去修改,所以就這樣保存下來了
ArrayList和HashSet也是這樣做的(事實上所有集合都是這樣的)

HashMap集合類的成員

成員變量

序列化版本號
在這裏插入圖片描述
集合的初始化容量
在這裏插入圖片描述
問題: 爲什麼必須是2的n次冪?如果輸入值不是2的冪比如10會怎麼樣?
在這裏插入圖片描述
根據上述講解我們已經知道,當向HashMap中添加一個元素的時候,需要根據key的hash值,去確定其在數組中的具體位置。 HashMap 爲了存取高效,要儘量較少碰撞,就是要儘量把數據分配均勻,每個鏈表長度大致相同,這個實現就在把數據存到哪個鏈表中的算法。
這個算法實際就是取模,hash%length,計算機中直接求餘效率不如位移運算,所以源碼中做了優化,使用 hash&(length-1)而實際上 hash%length 等於 hash&(length-1) 的前提是 length 是2的n次冪

爲什麼這樣能均勻分佈減少碰撞呢?
length 取2的n次冪方便使用位運算提高效率,2的n次方實際就是1後面n個0,2的n次方-1 實際就是n個1
舉例:

公式:hash&(length-1) 

假設數組長度是8  0000 1000 (8的二進制)		|			假設數組長度是7 0000 0111 (7的二進制)
										|			
hash = 1								|			hash = 1
0000 0001								|			0000 0001
0000 0111	(8-1的二進制)				|			0000 0110	(7-1的二進制)	
0000 0001 --- 1	(&運算)					|			0000 0000 --- 0 (&運算)
								   	   下同				
hash = 2								|			hash = 2
0000 0010								|			0000 0010
0000 0111								|			0000 0110
0000 0010 --- 2							|			0000 0010 --- 2
										|
hash = 3								|			hash = 3
0000 0011								|			0000 0011
0000 0111								|			0000 0110
0000 0011 --- 3							|			0000 0010 --- 2
										|
hash = 4								|			hash = 4
0000 0100								|			0000 0100
0000 0111								|			0000 0110 
0000 0100 --- 4							|			0000 0100 --- 4

可見當 length=8 的時候,hash=3 和 4 時結果是不同的,length=7 的時候,hash=3 和 4 時結果卻相同,說明了 length 取2的n次冪能均勻分佈減少碰撞,但如果不考慮效率,直接求餘數的話,就不需要要求長度是2的n次冪。

關於 length 取2的n次冪的總結:
  1. 由上面可以看出,當我們根據key的hash確定其在數組的位置時,如果數組長度爲2的n次冪,就可以保證數據的均勻插入 。如果不是2的n次冪,可能數組的一些位置永遠不會插入數據,浪費數組的空間,加大了hash碰撞。

  2. 另一方面,一般我們可能會想通過 % 求餘數來確定位置,這樣做其實也是可以的,但是性能不如 & 位與運算而且當數組長度是2的n次冪時,hash & (length - 1) == hash % length

  3. HashMap容量是2的n次冪的原因:是爲了數據的均勻分佈,減少Hash衝突。Hash衝突越大,代表數組中一個鏈表就越長,這樣會降低hashmap的性能

重點:如果在創建HashMap對象的時候,指定的容量不是2的n次冪,比如10,HashMap會通過一系列位移運算和或運算得到一個2的n次冪,這個數字是大於並且離我們指定容量最近的數字(10的話就會得到16)

問題:爲什麼要找大於指定容量最近的2的n次冪而不是直接找指定容量最近的2的n次冪?

答:如果找到的2的n次冪小於我們指定的容量,就很可能會執行擴容操作耗費更多時間,降低了程序效率,得不償失

HashMap的部分源碼

	/**
     * 用指定的容量和負載因子初始化一個HashMap
     *
     * @param initialCapacity 初始值
     * @param loadFactor      負載因子
     * @throws IllegalArgumentException if the initial capacity is negative
     *                                  or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        // 判斷初始化容量 initialCapacity 是否小於0
        if (initialCapacity < 0) {
            // 如果小於 0,拋出非法的參數異常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        }
        // 判斷初始化容量 initialCapacity 是否大於集合的最大容量 MAXIMUM_CAPACITY
        // 最大容量:static final int MAXIMUM_CAPACITY = 1 << 30;
        if (initialCapacity > MAXIMUM_CAPACITY) {
            // 如果超過最大容量,將最大容量賦值給 initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
        }
        // 判斷加載因子 是否小於等於0,或者是否是一個非法數值
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            // 如果滿足上面條件,拋出非法參數異常
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        }
        // 將指定的負載因子賦值給 loadFactor
        this.loadFactor = loadFactor;
        /*
            tableSizeFor 判斷指定的初始化容量是否爲 2 的n次冪,
            如果不是,那就變爲比指定容量大的最小的2的n次冪。
            但是注意,這裏計算出初始化容量之後,直接賦值給了threshold
            有人認爲這是個bug
            事實上,在put方法中,會對threshold重新計算
         */
        this.threshold = tableSizeFor(initialCapacity);
    }
    
	/**
     * 該方法作用是讓HashMap的容量永遠是2的n次冪
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        // | (位或運算符)運算規則:兩個數都轉爲二進制,然後從高位開始比較,兩個數只要有一個爲1則爲1,否則就爲0。
        /*
        >>> 表示符號位也會跟着移動,比如 -1 的最高位是1,表示是個負數,然後右移之後,最高位就是0表示當前是個正數。
        所以	-1 >>>1 = 2147483647
        >> 表示無符號右移,也就是符號位不變。那麼-1 無論移動多少次都是-1
        原理就是將最高位 1 右邊的所有比特位全置爲 1,然後再加 1,最高位進 1,右邊的比特位全變成 0,從而得出一個 2 的次冪的值
         */
        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;
    }

說明: 由此可以看到,當在實例化HashMap的時候,如果給定了initialCapacity(假設是10),由於HashMap的initialCapacity必須都是2的n次冪,因此這個方法用於找到大於等於initialCapacity的最小的是2的n次冪。如果initialCapacity已經是2的n次冪,則直接返回這個數。

分析:

  1. 爲什麼要進行cap - 1 的操作: Int n = cap - 1.這是爲了防止cap已經是2的n次冪。如果cap已經是2的n次冪,又沒有執行減1的操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個數的2倍。

  2. 如果這時候n是0,最後返回的是capacity是1,因爲最後有個n+1的操作。

  3. 按位或運算符 | 運算規則: 相同位置的二進制數位上,如果都是0,那麼結果就是0,否則爲1

// tableSizeFor(int cap)函數執行過程中n的值

int n = cap - 1; // 初始值:cap = 10,n = 9

n |= n >>> 1
00000000 00000000 00000000 00001001 // n的原值:9
00000000 00000000 00000000 00000100 // 右移一位後的值:4
00000000 00000000 00000000 00001101 // 原值與右移後的值進行或運算的值:13
第一次位移後 n = 13

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


第二次右移
n |= n >>> 2 
00000000 00000000 00000000 00001101 // 13
00000000 00000000 00000000 00000011 // 右移兩位後的值:3
00000000 00000000 00000000 00001111 // 原值與右移後的值進行或運算的值:15
第二次右移後 n = 15

注意:這個n已經經過了 n |= n >>> 1的操作,假設此時 n 爲 00000000 00000000 00000000 00001101,則無符號右移2位,會將最高位兩個連續的1右移兩位,然後再與原本的n進行或操作,這樣n的二進制表示中,最高位會有4個連續的1。


第三次右移
n |= n >>> 4 
00000000 00000000 00000000 00001111 // 15
00000000 00000000 00000000 00000000 // 右移四位後的值:0
00000000 00000000 00000000 00001111 // 原值與右移後的值進行或運算的值:15
第三次右移後 n = 15

這次把已有的高位中的連續的4個1,右移4位,再做或操作。這樣n的二進制位表示的高位中,正常會有8個連續的1。
注意:容量最大也就是32位的正數,因此最後只有 n |= n >>> 16,最多就是32個1。但是,這時候32個1是負數。所以在執行tableSizeFor之前,會對initialCapacity進行判斷。如果大於 1 << 30,則取最大值 MAXIMUM_CAPACITY。如果結果等於最大值,就會執行位移操作


第四次、第五次位移結果和第三次相同,故省略
...


最後返回 n + 1,故 tableSizeFor(10) 的返回值爲16

執行多次位移運算和或運算的目的,是讓低位全部都成1,在最後會有一個 n + 1 操作,使我們最終計算的結果是 2的n次冪

其他成員變量及負載因子

默認的負載因子,默認值是0.75
在這裏插入圖片描述
當 HashMap 中元素的個數 >= 數組長度 * 負載因子,就會擴容數組。
負載因子在使用過程中不建議改變,所以不推薦使用 HashMap 中能傳入負載因子的構造函數
public HashMap(int initialCapacity, float loadFactor) 不推薦使用

使用TreeNode的臨界值
在這裏插入圖片描述

當鏈表的值小於6則會從紅黑樹轉回鏈表
在這裏插入圖片描述

當Map裏面的數量超過這個值時,表中的桶才能進行樹形化 ,否則桶內元素太多時會擴容,而不是樹形化
在這裏插入圖片描述

table用來初始化(必須是二的n次冪)(重點)
在這裏插入圖片描述
Table 在 JDK1.8 中我們瞭解到,是由數組+鏈表+紅黑樹來組成結構。JDK1.8 之前,這個類型叫 Entry。實際上也只是改了個名字而已。二者都是實現了一樣的接口,都是 Map.Entry

用來存放緩存
在這裏插入圖片描述

HashMap中存放元素的個數(重點)
在這裏插入圖片描述
注意:這裏的size是標識HashMap中key-value的數量,而不是數組的長度

用來記錄HashMap的修改次數
在這裏插入圖片描述

用來調整大小下一個容量的值計算方式爲(容量 * 負載因子)
在這裏插入圖片描述
哈希表的加載因子(重點)
在這裏插入圖片描述

說明

  1. loadFactor(負載因子),是用來衡量 HashMap 滿的程度,標識HashMap的疏密程度, 影響 hash 操作到同一個數組位置的概率,計算 HashMap 的實時加載因子的方法爲:size / capacity

  2. loadFactor(負載因子)太大會導致查找元素效率低,太小導致數組的利用率低,存放的數據會很分散。 0.75是官方經過大量的數據測試,得出的最好的數字

  3. 當 HashMap 中容納的元素超過邊界值時,認爲 HashMap 太擠了,就需要擴容。這個擴容的過程涉及到rehash(重新計算 hash 值)複製等操作,非常的消耗性能,所以開發中儘量減少擴容的次數,可以通過創建 HashMap 時指定初始化容量來儘量的避免

比如: 我們需要存放1000個元素到 HashMap,那麼我們可能需要 new HashMap(1024)。但是1024*0.75=768<1000,就會擴容,因此這個時候我們應該 new HashMap(2048)。所以開發中如果我們能預測要存放的元素個數,應當指定一個合適的值來避免執行擴容操作,以提高程序性能

HashMap的構造方法

  1. 空參構造,構造一個空的HashMap,默認負載因子是0.75,JDK7中,在我們 new HashMap 的時候,會立即創建 Hash 桶,而在JDK8中,在 new HashMap 時,並不會創建數組,而是在 put 方法中,先判斷 table 是否爲空

    /**
     * 用默認的容量去初始化一個HashMap
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    
  2. 構造一個指定初始容量和默認負載因子的HashMap(建議使用)

    /**
     * 指定容量去初始化一個HashMap
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
  3. 構造一個具有指定容量和負載因子的HashMap(不建議使用)

    /**
     * 用指定的容量和負載因子初始化一個HashMap
     *
     * @param initialCapacity 初始值
     * @param loadFactor      負載因子
     * @throws IllegalArgumentException if the initial capacity is negative
     *                                  or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        // 判斷初始化容量 initialCapacity 是否小於0
        if (initialCapacity < 0) {
            // 如果小於 0,拋出非法的參數異常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        }
        // 判斷初始化容量 initialCapacity 是否大於集合的最大容量 MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY) {
            // 如果超過最大容量,將最大容量賦值給 initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
        }
        // 判斷加載因子 是否小於等於0,或者是否是一個非法數值
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            // 如果滿足上面條件,拋出非法參數異常
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        }
        // 將指定的負載因子賦值給 loadFactor
        this.loadFactor = loadFactor;
        /*
            tableSizeFor 判斷指定的初始化容量是否爲 2 的n次冪,
            如果不是,那就變爲比指定容量大的最小的2的n次冪。
            但是注意,這裏計算出初始化容量之後,直接賦值給了threshold
            有人認爲這是個bug
            事實上,在put方法中,會對threshold重新計算
         */
        this.threshold = tableSizeFor(initialCapacity);
    }
    
  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>.
     *
     * @param m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
    /**
     * 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) {
        // 獲取map的元素個數
        int s = m.size();
        if (s > 0) {
            // 判斷 table是否已經初始化
            if (table == null) {
                // 未初始化, s是m的元素個數
                /*
                    假設 s 是 6,s / loadFactor = 8
                 */
                float ft = ((float) s / loadFactor) + 1.0F;
                int t = ((ft < (float) MAXIMUM_CAPACITY) ?
                        (int) ft : MAXIMUM_CAPACITY);
                // 判斷得到的值是否大於閾值,如果大於閾值,則初始化閾值
                if (t > threshold) {
                    threshold = tableSizeFor(t);
                }
            } else if (s > threshold) {
                // 已初始化,並且元素個數大於閾值,進行擴容
                resize();
            }
            // 將m中所有的元素添加到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);
            }
        }
    }
    

    注意:loat ft = ((float) s / loadFactor) + 1.0F; 爲什麼要 +1F
    結論:s / loadFactor結果是小數,+1.0F作用是相當於給小數向上取整,儘可能保證更大容量。更大的容量能夠減少resize次數。

HashMap的成員方法

put 方法

put 方法是比較複雜的,實現步驟大致如下:

  1. 先通過 hash 值計算出 key 映射到哪個桶

  2. 如果桶上沒有碰撞衝突,則直接插入

  3. 如果出現碰撞衝突了,則需要處理衝突
    ​ a:如果該桶使用紅黑樹處理衝突,則調用紅黑樹的方法插入數據
    ​ b:否則採用傳統的鏈式方法插入。如果鏈的長度達到臨界值,則把鏈轉變爲紅黑樹

  4. 如果桶中存在重複的鍵,則爲該鍵替換新值value

  5. 如果size大於閾值threshold,則進行擴容;

具體的 put 方法

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key   key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     * <tt>null</tt> if there was no mapping for <tt>key</tt>.
     * (A <tt>null</tt> return can also indicate that the map
     * previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    @Override
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

說明:

  1. HashMap 只提供了 put 用於添加元素,putVal 方法只是給 put 方法調用的一個方法,並沒有提供給用戶使用。 所以我們重點看 putVal 方法。

  2. 我們可以看到在 putVal() 方法中 key 在這裏執行了一下hash()方法,來看一下 Hash 方法是如何實現的。

hash 方法

	static final int hash(Object key) {
        int h;
        /*
            如果key爲null
                可以看到當key爲null的時候也是有哈希值的,返回值是0
            如果key不爲null
                首先計算出key的hashCode,然後賦值給h,接着,h進行無符號右移16位,再進行異或運算
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

從上面我們可以得知,HashMap 是 支持null key的,而HashTable是直接使用 key 來獲取 hashCode,但 key 爲空會拋異常,所以當 key 爲空時賦值爲 0
其實我們上面已經解釋了爲什麼 HashMap 的長度要是2的n次冪,因爲 HashMap 設計的非常巧妙,它通過 hash & (length - 1) 來得到該對象的保存位,等價於 hash % length,但是 & 效率比 % 要高

探究:關於 h 右移16位的操作
putVal 中 會使用 (length - 1) & hash 來進行運算
在這裏插入圖片描述

  1. key.hashCode,返回散列值也就是hashCode,假設生成一個值

  2. n 表示數組長度,16(默認16)

  3. ^ 異或運算符,運算規則:相同的二進制數位上,數字相同,結果爲0,否則爲1

過程:

代碼:(h = key.hashCode()) ^ (h >>> 16)

假設 h = key.hashCode() 結果是
1111 1111 1111 1111 1111 0000 1110 1010 // 調用hashCode計算出的結果

進行 h >>> 16 的操作 
0000 0000 0000 0000 1111 1111 1111 1111 // h右移16位的結果

h = key.hashCode() 和 h >>> 16 的結果進行 ^(異或)操作
1111 1111 1111 1111 1111 0000 1110 1010
0000 0000 0000 0000 1111 1111 1111 1111
1111 1111 1111 1111 0000 1111 0001 0101 // 計算hash

putVal方法中進行 (n- 1) & hash 操作,假設 n= 16, 則 n - 1 = 15
0000 0000 0000 0000 0000 0000 0000 1111 // 15
1111 1111 1111 1111 0000 1111 0001 0101
0000 0000 0000 0000 0000 0000 0000 0101 // 計算下標結果是 5

說明:高16位不變,低16位和高16位進行了一個異或運算。

問題:爲什麼要進行 h 右移16位的操作

假設不進行16位右移操作

(n - 1) 與 h 進行 & 操作
0000 0000 0000 0000 0000 0000 0000 1111 // n = 15
1111 1111 1111 1111 1111 0000 1110 1010	// h = key.hashCode()
0000 0000 0000 0000 0000 0000 0000 1010 // 10


0000 0000 0000 0000 0000 0000 0000 1111 // n = 15
1010 0110 0011 1111 1111 0000 1110 1010	// h = key.hashCode()
0000 0000 0000 0000 0000 0000 0000 1010 // 10

當 h 不右移16位時,即使 h 的值不同,但putVal方法中的 (n- 1) & hash 的結果卻很容易相同,說明 h 不右移16位容易出現 hash 碰撞

如果當 n(數組長度) 的長度很小,假設是16,那麼 n-1 的後四位爲 1111 ,這樣的值和 hashCode 直接做按位與操作,實際上只使用了哈希值的後4位,高位將沒有任何意義。 如果當哈希值高位變化很大,低位變化很小,這樣就非常容易造成哈希衝突,所以這裏要把高位和低位都利用起來,從而解決這個問題。 說白了,這個操作的作用,其實就是爲了防止哈希衝突

putVal 方法

 	/**
     * Implements Map.put and related methods.
     *
     * @param hash         key的hash值
     * @param key          原始key
     * @param value        key對應的value
     * @param onlyIfAbsent 如果爲true代表不更改現有的值
     * @param evict        如果爲false,表示table爲創建狀態
     * @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;
        // n存放數組長度。i存放key的hash計算後的值
        int n, i;
        /*
            判斷table是否爲空
            1. table 表示存儲在Map中的元素的數組
            2. (tab = table) == null 表示將table賦值給tab,並且判斷tab是否爲null。
            3. (n = tab.length) == 0 表示,將tab的長度賦值給n,並判斷n 是否等於-
         */
        if ((tab = table) == null || (n = tab.length) == 0) {
            // 如果爲空就通過resize實例化一個數組
            /*
                這裏的代碼等價於
                tab = resize();
                n = tab.length
             */
            n = (tab = resize()).length;
        }
        /*
            i = hash & (n - 1) 計算當前key所在下標,確定在哪個桶中,並將下標賦值給i
            p = tab(i) 將該位置的元素賦值給p,並且判斷是否爲null
         */
        if ((p = tab[i = (n - 1) & hash]) == null) {
            // 直接創建一個Node元素,賦值給當前下標位置
            tab[i] = newNode(hash, key, value, null);
        } else {
            // 當前下標位置不爲null
            // 注意,我們在上面的if中,已經把當前下標位置的元素,賦值給了p
            Node<K, V> e;
            K k;
            /*
                比較桶中第一個元素的hash值和key是否相等。
                1. p.hash == hash :判斷第一個元素的hash與我們傳進來的hash是否相等
                2. ((k = p.key) == key || (key != null && key.equals(k)))
                    2.1 (k = p.key) == key 將第一個元素的key賦值給k,並且判斷是否和我們傳進來的key相等
                    2.2 判斷我們傳進來的key不等於null,並且key的值和k相等
                 上面如果都滿足的情況下,說明第一個元素的key和我們傳進來的key值是相等的
             */
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k)))) {
                // 將該位置的節點賦值給e
                e = p;
            } else if (p instanceof TreeNode) {
                // 判斷當前下標位置的數據類型是否爲紅黑樹
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            } else {
                // 說明當前元素是個鏈表
                // 不是紅黑樹,當前下標位置的key也與要插入的key不相等
                // 遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    /*
                        (e = p.next) == null 將p的下一個元素賦值給e,並判斷e是否爲null
                        如果等於null,說明當前元素是表尾,已經沒有下一個元素了
                        如果不爲null,說明下一個元素還存在,可以繼續遍歷
                     */
                    if ((e = p.next) == null) {
                        // 進入,說明e是表尾
                        // 直接將數據寫到下一個節點
                        p.next = newNode(hash, key, value, null);
                        /*
                            1. 節點添加完成之後判斷此時節點個數是否大於臨界值 8,如果大於則將鏈表轉爲紅黑樹。
                            2. int binCount = 0,表示for循環的初始化值,從0開始計算,記錄遍歷節點的個數
                                |- 0表示第一個節點
                                |- 1表示第二個節點
                                |- 。。。。
                                |- 7表示第八個節點
                                因此這裏TREEIFY_THRESHOLD需要-1
                         */
                        if (binCount >= TREEIFY_THRESHOLD - 1) {
                            // 將鏈表轉爲紅黑樹
                            treeifyBin(tab, hash);
                        }
                        break;
                    }
                    // 如果當前位置的key與要存放位置的key相同,直接跳出
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        /*
                            要添加的元素和鏈表中存在的元素相等了,則跳出for循環,不需要再比較後面的元素了
                            直接進入下面的if語句去替換e的值
                         */
                        break;
                    }
                    // 說明新添加的元素和當前節點不相同,繼續找下一個元素。
                    p = e;
                }
            }
            // e不爲空,說明上面找到了一個去存儲Key-Value的Node
            if (e != null) {
                // 拿到舊Value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) {
                    // 新的值賦值給節點
                    e.value = value;
                }
                afterNodeAccess(e);
                // 返回舊value
                return oldValue;
            }
        }
        // 統計數據改變次數
        ++modCount;
        // 當最後一次調整之後的Size大於臨界值,就需要調整數組容量
        if (++size > threshold) {
            resize();
        }
        afterNodeInsertion(evict);
        return null;
    }

數組擴容 resize

問題:
  1. 什麼時候才需要擴容
    當Hashmap中元素個數超過 數組長度*負載因子 就會進行擴容。也就是說,當HashMap數組長度是16的時候,如果元素個數超過 16 * 0.75=12 的時候,就會給數組擴容,擴容方式就是將原數組擴大2倍,也就是16 * 2=32,然後重新計算每個元素在數組中的位置。而這是個非常消耗性能的過程所以當我們已經預知到HashMap中元素個數的時候,應當賦予一個基本不可能讓這個HashMap擴容的長度。
    注:當HashMap中的其中一個鏈表的對象個數達到了8個,此時如果數組長度沒有達到64,那麼HashMap也會進行擴容。如果達到了64,那麼這個鏈表會變成紅黑樹,節點類型由Node變成TreeNode。

  2. HashMap的擴容是什麼
    進行擴容,會伴隨着一次重新的Hash分配,並且會遍歷hash表中的所有元素,是非常耗時的,所以在編寫代碼的過程中,應當儘量的避免Hashmap的resize。
    注:HashMap在進行擴容的時候,使用rehash非常的巧妙。因爲,每次擴容都是翻倍,與原來的 (n-1)&hash 的結果相比,只是多了一個二進制位,所以節點要麼在原來的位置,要麼就被分配到 原位置+原容量 這個位置。
    比如我們從16擴容到32

    n = 16  ,n-1=15
    0000 0000 0000 0000 0000 0000 0001 0000  n = 16
    0000 0000 0000 0000 0000 0000 0000 1111  n - 1 = 15
    1111 1111 1111 1111 1111 1111 0000 0101  假設存在的元素key1的位置
    1111 1111 1111 1111 1111 1111 0001 0101  假設存在的元素key2的位置
    ===================進行 (n-1) & hash 操作======================
    0000 0000 0000 0000 0000 0000 0000 0101  key1下標 --- 5
    0000 0000 0000 0000 0000 0000 0000 0101  key2下標 --- 5
    
    n擴容 ===> n = 32  , n - 1 = 31
    0000 0000 0000 0000 0000 0000 0010 0000  n = 32
    0000 0000 0000 0000 0000 0000 0001 1111  n - 1 = 31
    1111 1111 1111 1111 1111 1111 0000 0101  key1
    1111 1111 1111 1111 1111 1111 0001 0101  key2
    ==============進行 (n-1) & hash 操作重新計算下標=================
    0000 0000 0000 0000 0000 0000 0000 0101  key1新下標 -- 5
    0000 0000 0000 0000 0000 0000 0001 0101  key2新下標 -- 5+16=21
    

    在元素重新計算hash之後,因爲n變爲2倍,那麼n-1的標記範圍在高位多1,因此我們的新index就會發生這樣的變化。
    在這裏插入圖片描述因此,我們在擴容HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就可以了。是0的話下標不變,是1的話下標變爲 原位置+舊容量。下圖是16擴容到32的示意圖。

    在這裏插入圖片描述 正是因爲這樣巧妙地rehash方式,省去了重新計算hash的時間,而且同時,因爲新增的1bit是0還是1可以認爲是隨機的,在resize過程中保證了rehash之後每一個桶上的節點數一定小於等於原來桶上的節點數,保證了rehash之後不會出現更嚴重的hash衝突,均勻的把之前的衝突的節點分散到新的桶中。

resize方法代碼

	/**
     * 數組擴容
     */
    final Node<K, V>[] resize() {
        // 先拿到舊的hash桶
        Node<K, V>[] oldTab = table;
        // 獲取未擴容前的數組容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 舊的臨界值
        int oldThr = threshold;
        // 定義新的容量和臨界值
        int newCap, newThr = 0;
        // 舊容量大於0
        if (oldCap > 0) {
            // 舊的容量如果超過了最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 臨界值就等於Integer類型最大值
                threshold = Integer.MAX_VALUE;
                // 不擴容,直接返回舊數組
                return oldTab;
            }
            /*
                沒超過最大值,數組擴容爲原來的2倍
                1.(newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之後賦值給newCap,判斷newCap是否小於最大容量
                2.oldCap >= DEFAULT_INITIAL_CAPACITY 原數組長度大於等於數組初始化長度
             */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY) {
                // 當前容量在默認值和最大值的一半之間
                // 新的臨界值爲當前臨界值的2倍
                newThr = oldThr << 1; // double threshold
            }
        } else if (oldThr > 0) // initial capacity was placed in threshold
        {
            // 舊容量爲0,當前臨界值不爲0,讓新的臨界值等於當前臨界值
            newCap = oldThr;
        } else {
            // 當前容量和臨界值都爲0,讓新的容量等於默認值,臨界值=初始容量*加載因子
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 經過上面對新臨界值的計算後如果還是0
        if (newThr == 0) {
            // 計算臨界值爲新容量 * 加載因子
            float ft = (float) newCap * loadFactor;
            // 判斷新容量小於最大值,並且計算出的臨界值也小於最大值
            // 那麼就把計算出的臨界值賦值給新臨界值。否則新臨界值默認爲Integer最大值
            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];
        // 賦值給hash桶
        table = newTab;
        // 下面一堆是複製值
        // 如果舊的桶不爲空
        if (oldTab != null) {
            // 遍歷舊桶,把舊桶中的元素重新計算下標位置,賦值給新桶
            // j 表示數組下標位置
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                /*
                   (e = oldTab[j]) != null 將舊桶的當前下標位置元素賦值給e,並且e不爲null
                 */
                if ((e = oldTab[j]) != null) {
                    // 置空,置空之後原本的這個數據就可以被gc回收
                    oldTab[j] = 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
                        // 到這裏說明該位置的元素是鏈表
                        /*
                        loHead:鏈表頭結點
                        loTail:數據鏈表
                        hiHead:新位置鏈表頭結點
                        hiTail:新位置數據鏈表
                         */
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        // 循環鏈表,直到鏈表末再無節點
                        do {
                            // 獲取下一個節點
                            next = e.next;
                            // 如果這裏爲true,說明e這個節點在resize之後不需要移動位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail != null) {
                                    loTail.next = e;
                                } else {
                                    loHead = 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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

remove和removeNode方法代碼

	/**
     * 根據key刪除元素
     * 刪除是有返回值的
     * 並且返回值是被刪除key所對應的value
     */
    @Override
    public V remove(Object key) {
        Node<K, V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }

	/**
     * 刪除方法的核心邏輯
     *
     * @param hash       hash for key
     * @param key        the key
     * @param value      the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable    if false do not move other nodes while removing
     * @return the node, or null if none
     */
    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;
        /*
            1. (tab = table) != null  把hash桶賦值給tab,並且判斷tab是否爲nul
            2. (n = tab.length) > 0 獲取tab的長度,賦值給n,判斷n是否大於0
            3. (p = tab[index = (n - 1) & hash]) != null 根據hash計算索引位置,賦值給index
                並從tab中取出該位置的元素,賦值給p,並判斷,p不爲null
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            // 進入這裏面,說明hash桶不爲空,並且當前key所在位置的元素不爲空
            Node<K, V> node = null, e;
            K k;
            V v;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k)))) {
                // 當前第一個位置的元素就是我們要找的元素
                node = p;
            }
            // 取出p的下一個節點賦值給e,並且e不爲空
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode) {
                    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
                } else {
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 判斷node不爲空,
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                if (node instanceof TreeNode) {
                    ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                } else if (node == p) {
                    // node==p,說明node是第一個節點,那麼直接將下一個節點賦值給當前下標
                    tab[index] = node.next;
                } else {
                    p.next = node.next;
                }
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

get方法代碼

 /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    @Override
    public V get(Object key) {
        Node<K, V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }


    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key  the key
     * @return the node, or null if none
     */
    final Node<K, V> getNode(int hash, Object key) {
        Node<K, V>[] tab;
        // first存放對應下標位置的第一個元素
        Node<K, V> first, e;
        int n;
        K k;
        /*
            1.(tab = table) != null 把table賦值給tab,並且判斷tab不爲空
            2.(n = tab.length) > 0 把tab的長度賦值給n,並且判斷n大於0
            3.(first = tab[(n - 1) & hash]) != null 根據傳進來的hash計算下標位置,取出該下標位置的元素賦值給first,並且判斷first不爲空
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            // 下標位置第一個元素的key就是我們要找的key
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k)))) {
                return first;
            }
            // 獲取下一個節點賦值給e,並且判斷e不爲空
            if ((e = first.next) != null) {
                if (first instanceof TreeNode) {
                    // 如果是紅黑樹,就用紅黑樹方式取值
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                }
                // 遍歷鏈表直到下一個節點不存在爲止
                do {
                    // 找到對應的key的位置
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        return e;
                    }
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

HashMap遍歷方式

 private HashMap<String, Integer> getMap() {
        HashMap<String, Integer> map = new HashMap<>(16);
        map.put("稽哥", 18);
        map.put("雷哥", 28);
        map.put("吳彥祖", 18);
        map.put("張學友", 40);
        map.put("郭德綱", 50);
        map.put("趙本山", 60);
        map.put("肖戰", 29);
        return map;
}

/**
 * 1、分別遍歷key和value
 */
@Test
public void testMap1() {
    HashMap<String, Integer> map = getMap();
    for (String key : map.keySet()) {
        System.out.println(key);
    }
    for (Integer value : map.values()) {
        System.out.println(value);
    }
}

/**
 * 2、使用iterator迭代器迭代
 * Map底層中增強for循環就是使用的迭代器
 */
@Test
public void testIterator() {
    HashMap<String, Integer> map = getMap();
    Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Integer> entry = iterator.next();
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

/**
 * 3、通過get方式
 * 說明:根據阿里開發手冊,不建議這種方式,因爲要迭代多次。keySet一次,get一次。
 */
@Test
public void testGet() {
    HashMap<String, Integer> map = getMap();
    Set<String> keySet = map.keySet();
    for (String key : keySet) {
        System.out.println(key + ":" + map.get(key));
    }
}

/**
 * 4、Jdk8以後使用Map接口中的一個默認方法
 */
@Test
public void testForeach() {
    HashMap<String, Integer> map = getMap();
    map.forEach((key, value) -> {
        System.out.println(key + ":" + value);
    });
}

HashMap使用細節

  1. HashMap初始化問題描述
    如果我們確切的知道我們有多少鍵值對需要存儲,那麼我們在初始化HashMap的時候就應該指定它的容量,以防止HashMap自動擴容,影響使用效率。
    默認情況下HashMap容量是16,如果用戶通過構造方法指定了一個數字作爲容量,那麼HashMap會選擇大於等於該數字的第一個2的n次冪作爲容量。
    《阿里巴巴Java開發手冊》中建議我們使用HashMap的初始化容量。
    在這裏插入圖片描述

  2. HashMap中容量的初始化
    當我們使用 HashMap(int initialCapacity) 來初始化容量的時候,jdk會默認給我們計算一個相對合理的值來當做初始容量。那麼,我們是不是直接把元素個數作爲initialCapacity就可以了呢?
    答案是否定的因爲在我們使用HashMap的過程中,隨着元素的數量不斷增大,HashMap會不斷地進行擴容。並且擴容條件是元素個數 = 數組長度 * 0.75

    比如我們需要存放1000個元素,那麼我們設置1000會有兩個不合理之處。
    (1)1000不是2的n次冪,設置爲1000,HashMap會給我們計算成1024
    (2)1024雖然是2的n次冪,但是 1024*0.75 < 1000 ,因此當我們使用的過程中,肯定會出現擴容,造成性能上的浪費,因此我們需要設置爲2048.
    在這裏插入圖片描述

紅黑樹相關

二叉查找樹

在這裏插入圖片描述
上面這張圖就是個二叉查找樹。二叉查找樹滿足下面幾條特徵。

1. 左子樹上所有節點的值均小於或者等於它的根節點的值

2. 右子樹上所有節點的值均大於或者等於它的根節點的值

3. 左右子樹也分別爲二叉查找樹

既然名字中帶有“查找”,那麼它是怎麼查找的呢?

比如我們要查找10這個元素,首先找到根節點,然後根據1、2特性,10>9,那麼繼續從右邊節點查找,10<13,那麼繼續從左邊節點查找,10<11,繼續查左邊節點,找到了10這個節點。

紅黑樹

上面我們說到了二叉查找樹的思想,那麼我們思考一個問題,如果我們要在9這個節點插入7、6、5、4、3,一個比一個小,就會成一條直線,也就是成了線性的查詢。爲了解決這個情況,就需要使用紅黑樹了。

紅黑樹是一種自平衡的二叉查找樹,每個節點都帶有顏色屬性,顏色是紅色或者黑色。在二叉查找樹的特徵以外,任何一條紅黑樹都有以下額外的特性

  1. 節點是紅色或者黑色

  2. 根節點一定是黑色

  3. 每個葉子結點(NIL節點)是黑色的

  4. 每個紅色節點的兩個子節點都是黑色的(從每個葉子到根的所有路徑上不可能有兩個連續的紅色節點)

  5. 從任意一個節點到其每個葉子節點的所有路徑包含相同數目的黑色節點
    在這裏插入圖片描述

紅黑樹查找

因爲紅黑樹是一個自平衡的二叉查找樹,查詢操作不會破壞紅黑樹的平衡,所以查找和二叉查找樹的查詢方式沒有區別。

  1. 從根節點開始,把根節點設置爲當前節點。

  2. 若當前節點爲空,則返回null。

  3. 若當前節點不爲空,用當前節點的key和查找key做比較。

  4. 若當前節點的key等於要查找的key,那麼該key就是查找目標,返回當前節點。

  5. 若當前節點key大於查找的key,把當前節點的左子節點設置爲當前節點,重複2.

  6. 若當前節點key小於查找的key,把當前節點的右子節點設置爲當前節點,重複2

		/**
         * Finds the node starting at root p with the given hash and key.
         * The kc argument caches comparableClassFor(key) upon first use
         * comparing keys.
         */
        final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
            TreeNode<K, V> p = this;
            do {
                int ph, dir;
                K pk;
                TreeNode<K, V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h) {
                    p = pl;
                } else if (ph < h) {
                    p = pr;
                } else if ((pk = p.key) == k || (k != null && k.equals(pk))) {
                    return p;
                } else if (pl == null) {
                    p = pr;
                } else if (pr == null) {
                    p = pl;
                } else if ((kc != null ||
                        (kc = comparableClassFor(k)) != null) &&
                        (dir = compareComparables(kc, k, pk)) != 0) {
                    p = (dir < 0) ? pl : pr;
                } else if ((q = pr.find(h, k, kc)) != null) {
                    return q;
                } else {
                    p = pl;
                }
            } while (p != null);
            return null;
        }

treeifyBin方法

在前面分析put方法的時候,節點添加完成之後就會判斷此時節點個數是否大於8,如果大於則將鏈表轉換爲紅黑樹。

	/**
     * 替換指定哈希表的所引出桶中的所有節點,除非表太小,否則將修改大小,
     */
    final void treeifyBin(Node<K, V>[] tab, int hash) {
        int n, index;
        Node<K, V> e;
        /*
            如果當前數組爲空,或者數組長度小於進行樹形化的閾值(64)就去擴容,而不是轉換爲紅黑樹。
            目的:如果數組很小,那麼轉換爲紅黑樹然後遍歷效率要低一些,這時候進行擴容,那麼重新計算哈希值
            鏈表的長度就有可能變短了,數據會放到數組中,這樣相對來說效率高一些
         */
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
            resize();
        } else if ((e = tab[index = (n - 1) & hash]) != null) {
            /*
                1.執行到這裏說明哈希表中數組長度大於閾值64,開始進行樹形化。
                2.(e = tab[index = (n - 1) & hash]) != null 通過當前key的hash計算當前key所在的下標位置,取出來賦值給e,判斷e不爲空
             */
            // hd:紅黑樹的頭結點。tl:紅黑樹的尾結點
            TreeNode<K, V> hd = null, tl = null;
            do {
                // 重新創建一個樹節點,內容和當前鏈表節點e一致
                TreeNode<K, V> p = replacementTreeNode(e, null);
                if (tl == null) {
                    // 將新創建的p節點賦值給紅黑樹的頭結點
                    hd = p;
                } else {
                    /*
                    p.prev = tl 將上一個節點p賦值給現在的p的前一個節點
                    tl.next = p 將現在的節點p作爲樹的爲節點的下一個節點
                     */
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            /*
                讓桶中第一個元素即數組中的元素指向新建的紅黑樹的節點,以後這個桶裏的元素就是紅黑樹,而不是鏈表
             */
            if ((tab[index] = hd) != null) {
                hd.treeify(tab);
            }
        }
    }

左旋、右旋、變色

前面我們講到紅黑樹能自平衡,它考的就是左旋、右旋、變色三個操作。

左旋:以某個節點作爲支點,,其右節點變爲旋轉節點的父節點,右節點的左節點變爲旋轉節點的右節點,其餘不變。
在這裏插入圖片描述
右旋:以某個節點作爲支點,其左節點變爲旋轉節點的父節點,左節點的右節點變爲旋轉節點的左節點,其餘不變。
在這裏插入圖片描述
變色:節點的顏色由紅變黑或者由黑變紅的過程

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