背景摘要:在Map集合中,最常用的集合就是HashMap集合了。相信各位也能脫口而出她的特徵,JDK7(以下簡稱爲7)和JDK8(以下簡稱爲8)源碼和實現不一樣。7底層由數組+單向鏈表實現。在這之前我們提到過基於數組和鏈表實現的兩個集合。ArrayList與LinkedList。那麼在8源碼中新增了紅黑樹這麼一個數據結構,由於其特性大大增加了查詢效率。同時HashMap也是無序且線程非安全。那麼今天基於7的源碼來三顧HashMap。一顧結構解析、二顧核心變量、三顧構造函數及首次擴容原理。
目錄
一、初探HashMap,結構解析
HashMap,她的構造爲單向鏈表與數組,鏈表的概念可參考 LinkedList 一文。首先我們先一張圖簡單看看她結構。
如圖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);
}
那麼重點講通過重載利用的傳遞初始大小,與負載因子的方法。
- 首先判斷初始容量是否小於0,如果小於拋出異常。
- 再次判斷是否大於最大擴容值,如果超過了該擴容值,則默認爲最大擴容值。
- 判斷擴容倍數值是否爲符合標準值,即正數。
- 設置加載因子(loadFactor)爲設置的加載因子(未傳遞爲默認0.75)。
- 設置閾值(threshold )爲初始容量。(未傳遞爲默認16)。
- 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 < size < max。傳遞默認16得出16
- 計算閾值。當前擴容值*擴展因子 默認是16*0.75。這裏做了三元判斷,能夠得出最大值爲默認最大值(MAXIMUM_CAPACITY)+1。而目前是未超過最大值,故爲12。
- 表格初始擴容爲構造方法初始設置擴容值大小。 默認: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()。