三顧HashMap,一顧結構,二顧變量,三顧構造函數及首次擴容原理

背景摘要:在Map集合中,最常用的集合就是HashMap集合了。相信各位也能脫口而出她的特徵,JDK7(以下簡稱爲7)和JDK8(以下簡稱爲8)源碼和實現不一樣。7底層由數組+單向鏈表實現。在這之前我們提到過基於數組和鏈表實現的兩個集合。ArrayListLinkedList。那麼在8源碼中新增了紅黑樹這麼一個數據結構,由於其特性大大增加了查詢效率。同時HashMap也是無序且線程非安全。那麼今天基於7的源碼來三顧HashMap。一顧結構解析、二顧核心變量、三顧構造函數及首次擴容原理。

HashMap

目錄

一、初探 HashMap結構解析

二、再探 HashMap核心變量

三、三顧 HashMap構造函數及首次擴容

構造函數

首次擴容


 一、初探HashMap,結構解析

HashMap,她的構造爲單向鏈表與數組,鏈表的概念可參考 LinkedList 一文。首先我們先一張圖簡單看看她結構。

圖1.1

 如圖1.1,我們可以看到HashMap是一個由數組裝載鏈表組成的集合。大家都知道HashMap初始擴容值默認爲16,那麼就代表初次擴容時,數組裏是含有16個空的鏈表的。

二、再探HashMap,核心變量

基於JDK1.7講解,多個JDK安裝切換方法請看 IDEA將當前項目JDK更改爲指定版本

瞭解了結構之後我們再來看她重要的變量

碼雲集合源碼分析項目地址:

https://gitee.com/yiang-hz/gather

https://gitee.com/yiang-hz/gather/tree/master/map/src/main/java/com/yiang/map 

/**
 * 默認初始容量 必須是2次冪 這裏爲 16 二進制
 * // aka 16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
 * 默認的負載因子  也就是擴容倍數
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 最大擴容值,如果超過了則爲該值 10個億
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 擴展因子
 */
final float loadFactor;
/**
 * 閾值,需要擴容時設置,實際HashMap存放的大小 當達到該閾值時進行擴容
 * 要調整大小的下一個大小值(容量*負載因子)
 * 如果表爲空,那麼表將會在擴容時創建
 */
int threshold;
DEFAULT_INITIAL_CAPACITY:默認初始容量,默認值爲16。必須爲2的次冪。通過1<<4二進制形式計算在計算機內是最快的。
DEFAULT_LOAD_FACTOR:默認負載因子,默認值爲0.75。即擴容倍數。
MAXIMUM_CAPACITY:最大擴容值,也就是2的30次冪。10個億。一般不存在超過該值情況。
loadFactor:負載因子,該值直接影響HashMap每次擴容後的容量。構造函數初始化該值。
threshold:閾值,構造函數初始化賦值。HashMap實際存放元素大小。計算公式爲(容量*負載因子)。
--構造賦值爲設置的初始容量(DEFAULT_INITIAL_CAPACITY),默認16。
--首次擴容爲16*0.75=12。二次擴容爲 16*2=32(實際大小) -> 32 * 0.75 = 24(閾值)
/**
 * 一個空的實例,擴容前共享
 * 實際上定義該空值用來轉換table,可讀性更強。
 */
static final YiangHashMap.Entry<?,?>[] EMPTY_TABLE = {};
/**
 * 初始化,Entry對象數組
 */
transient YiangHashMap.Entry<K,V>[] table = (YiangHashMap.Entry<K,V>[]) EMPTY_TABLE;
/**
 * 數據表中包含的鍵-值映射的數目。即數組大小
 */
transient int size;
EMPTY_TABLE:一個空的數組,沒有實際意義,用來增加可讀性
table:數據表。也就是圖1.1示列的數組。首次擴容時即爲16。二次擴容爲32。數組大小以2倍擴容。
size:數組大小,即數組中實際存放的鏈表數量。

以上即核心八個變量,下面再解剖一下構造函數的賦值,及首次擴容。

三、三顧HashMap,構造函數及首次擴容

HashMap源碼中,通過無參與有參構造方法,來實現可自定義初始容量的方式來使用HashMap,那麼來一探究竟

3.1、構造函數

無參構造:初始容量爲默認值16,負載因子爲默認值0.75。

public YiangHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

單參構造:傳遞自定義初始容量。負載因子爲默認值0.75。

public YiangHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

那麼重點講通過重載利用的傳遞初始大小,與負載因子的方法。

  1. 首先判斷初始容量是否小於0,如果小於拋出異常。
  2. 再次判斷是否大於最大擴容值,如果超過了該擴容值,則默認爲最大擴容值。
  3. 判斷擴容倍數值是否爲符合標準值,即正數。
  4. 設置加載因子(loadFactor)爲設置的加載因子(未傳遞爲默認0.75)。
  5. 設置閾值(threshold )爲初始容量。(未傳遞爲默認16)。
  6. init()方法,鉤子函數,當前類未實現,給予子類實現其它操作。
    /**
     * HashMap初始化構造函數源碼解析
     * @param initialCapacity 初始化擴展容量 默認 16
     * @param loadFactor 加載因子默認 0.75
     */
    public YiangHashMap(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;
        threshold = initialCapacity;
        //子類的初始化鉤子。沒有實現的方法,給予子類繼承實現其他操作。模板模式
        //init();
    }

看完之後我們可能會有疑問:爲什麼閾值在構造函數賦值會爲16?有什麼含義嗎?爲什麼不是數組大小的0.75倍(12)?

還有這事?那我們看看接下來的首次擴容,來了解其原因。

3.2、首次擴容

總所周知,HashMap是在存值時開始擴容的,也就是說在構造函數執行完後,數組的大小還是一個默認值空對象的。那麼在源碼put方法中有這麼一段代碼。如果table爲空,那麼開始初始化數組。也就代表數組是在第一次存值時開始擴容的。那麼第一次擴容時,擴容大小的size 是賦值給閾值(threshold)的,在首次擴容時正是傳遞了該參數。所以我們瞭解閾值在構造參數時做了一個承載數組大小的作用。

//如果table爲空,那麼初始化數組大小
if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

再來看看擴容方法執行的步驟:

  1. 通過內部方法(三元表達式)判斷是否大於最大值,並且判斷是否小於1。範圍在 1 < size < max。傳遞默認16得出16
  2. 計算閾值。當前擴容值*擴展因子 默認是16*0.75。這裏做了三元判斷,能夠得出最大值爲默認最大值(MAXIMUM_CAPACITY)+1。而目前是未超過最大值,故爲12。
  3. 表格初始擴容爲構造方法初始設置擴容值大小。 默認:16
/**
 * 擴容table -- Inflates the table.
 * @param toSize 大小
 */
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize 計算值,三元的規約判斷 計算
    // 結果: 1 < n < Max 三種情況 所以首次擴容這裏爲16
    int capacity = roundUpToPowerOf2(toSize);
    //計算最小值: 16 * 0.75 = 12 , 最大值 10億 + 1 那麼threshold首次擴容值就是12
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //表格以capacity大小進行初始化,這裏capacity爲16
    table = new YiangHashMap.Entry[capacity];
}

通過以上擴容源碼我們瞭解,閾值(threshold)雖然爲數組大小的0.75,但在構造方法執行完成時,是爲首次擴容大小的,而數組實際上是一個空數組。在首次擴容時,將擴容大小賦值給數組。然後將該值通過與負載因子計算得出閾值的。這也就詮釋了上文“爲什麼閾值在構造函數賦值會爲16?”這個問題。


總結:

HashMap的結構由單向鏈表 + 數組組成。擴容因子默認爲0.75,數組初始容量默認爲16,但實際存放大小(閾值)默認爲12。最大擴容值爲2^30。擴容是在首次存放鍵值入集合時開始,而不是在創建時就進行擴容。創建時只針對初始容量賦值給閾值做承載。並賦值對應的加載因子。且包含子類實現的鉤子函數init()。

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