問題引入
注:本文代碼源自java 9
。
- 阿里的插件對於初始化HashMap時,調用無參構造方法,提示如下:
- 那么問題來了,如果已知需要向 map 中 put n次,那么需要設定初始容量為多少?
- 單純的我今天上午還認為是合理的容量是 n + 1 即可,直到看了源碼;
- 應注意,
map.size
獲取的是當前map中鍵值對的個數,而不是容量。
當初始化的時候,沒有指定容量,情況如何?
- 直接調用如下構造函數(無參)
/**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 首先應該清楚,loadFactor 是 裝載因子(通俗來講,若值為0.5f,那么當map元素達到 容量*0.5f的時候,就應該做擴容等操作)
- HashMap對象實際上只有 threshold(臨界值,閾值) 屬性,每次put()后,需要比較的是閾值與size(map中已有鍵值對的個數)
- 至於容量屬性,實際上各個方法中用的是
transient Node<K,V>[] table;
中 table 的長度 - Hashmap結構可參考 http://www.importnew.com/20386.html
- 本構造方法上說明:構造了一個空的HashMap,默認容量為16,裝載因子為0.75。
- 然而可以清楚地看到,此構造方法只是將常量
DEFAULT_LOAD_FACTOR
賦值給裝載因子,但沒有設定容量。 - 當調用
put()
時,如下方式設定了初始容量。 - 調用
putVal(hash(key), key, value, false, true)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 在putVal()中,進入第一個if,調用resize()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//省略無關代碼
}
- 在下方代碼中,oldCap 與 oldThr 初始都為0,故直接進入else{}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0)
//省略無關代碼
else if (oldThr > 0) // initial capacity was placed in threshold
//省略無關代碼
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//設定 newCap = 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//設定 newThr = 16 * 0.75 = 12
}
threshold = newThr;//將對象的 threshold(臨界值,閾值) 屬性設置為 newThr
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = //將對象的 table 屬性設置為剛實例化的 newTab
//省略無關代碼
return newTab;//並返回
- 綜上,默認無參的構造方法設定裝載因子為0.75f,初始容量為16在調用put()時,設置為16。
當我們指定容量為n的時候,情況如何?
-
即:
Map<String,String> map = new HashMap<>(n);
-
當指定容量時,調用如下構造方法:
/**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 此方法將自定義容量作為參數1,將默認常量0.75f作為參數2,調用如下構造方法。
public HashMap(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;
this.threshold = tableSizeFor(initialCapacity);
}
- 此方法先做了對於容量與裝載因子值的合理性做了判斷與處理;
- 設定裝載因子值為0.75f,設置threshold 屬性為 tableSizeFor()參數為n時的返回值。
- 對於
tableSizeFor(initialCapacity)
,如下:
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}
- 也就是傳入 n 的時候,返回了 第一個 比 n 大的 2的冪(2.4.8.16.32.64...)的數,例 n為 10,返回16;n為17 返回 32.
- 具體實現原理可參考:http://blog.csdn.net/fan2012huan/article/details/51097331
- 同上,對於在put方法中,對象的table屬性仍然為null,還是需要resize();
- 本次調用與上次不同在於,threshold屬性 已經被賦值,假設值為
m
(m>0);
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap 值 為 0
int oldThr = threshold;//oldThr 值 為 m,m > 0
int newCap, newThr = 0;
if (oldCap > 0) {
//省略無關代碼
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;//newCap 被賦值為 m
//省略無關代碼
if (newThr == 0) {
float ft = (float)newCap * loadFactor;//ft為m*0.75
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
//在newCap 與ft 小於最大容量時(不滿足時不討論),執行 newThr = ft
}
threshold = newThr;//threshold 被再次賦值為 ft--->m*0.75
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;//容量賦值為 m
//省略無關代碼
return newTab;
}
- 綜上:當我們構造新的HashMap的時,傳入容量為n(假設為9),構造方法根據n得到閾值m(16)
當調用put()時,直接將閾值設置為m*0.75(12) ,將容量設置為 m(16) - 也就是說,當n不是2的冪的時候,會根據n找到合適的數 m,並認為m是新的容量,閾值是m*0.75!
說了那么多,初始化的時候,到底怎么設置參數n
- 如下是put方法的最后一步,需要進行的如下方式的驗證:
if (++size > threshold)
resize();
- 為了防止調用resize(),需要保證元素數量小於閾值(不可等於!)。
- 綜上,需要根據調用put(key,value)的次數
count
(准確的說是key不重復時),找到 第一個 比 n 大的 2的冪的數m
判斷裝載因子loadFactor(默認0.75)*m是否仍然不小於count
,若滿足則設置為count
,否則設置為2*m
或者m+1
- 例1,當我們需要在map里面裝 100個元素時,已知128是第一個大於100並且是2的冪的數,但128*0.75 = 96 還是小於100,
那么顯然129-256(閉區間)作為初始化的參數更合適。 - 例2,當我們需要在map里面裝 80個元素時,已知128是第一個大於80並且是2的冪的數,且128*0.75 = 96 還是大於80,
那么顯然64-128(左開右閉)作為初始化的參數更合適。 - 例3,當我們需要在map里面裝 12個元素時,應選用16為初始容量!
參考文章:http://blog.csdn.net/fan2012huan/article/details/51087722
http://yikun.github.io/2015/04/01/Java-HashMap工作原理及實現/
https://www.cnblogs.com/coderxuyang/p/3718856.html