public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable{
// 默認的初始容量(容量爲HashMap中桶的數目)是16,且實際容量必須是2的整數次冪。
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認加載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//... 省略
}
通過以上源碼可以看到在源碼中定義了一下幾個常量:
- 默認加載因子:這東西說白了就是用來劃分整個HashMap容量的百分比,這裏默認0.75就是說佔用總容量的75%
- 默認初始容量:如果你不在構造函數中傳值,new一個HashMap,他的容量就是2的4次方(16),並且增長也得是2的整數次方(冪)
- 閥值:首先這個值等於默認加載因子和初始容量的乘機;他的作用是用來預警的,如果HashMap中的容量超過這個閥值了,那就會執行擴容操作,低於則沒事
很多人忽視的加載因子Load Factor
加載因子存在的原因,還是因爲減緩哈希衝突,如果初始桶爲16,等到滿16個元素才擴容,某些桶裏可能就有不止一個元素了。所以加載因子默認爲0.75,也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。
考慮加載因子地設定初始大小
相比擴容時只是System.arraycopy()的ArrayList,HashMap擴容的代價其實蠻大的,首先,要生成一個新的桶數組,然後要把所有元素都重新Hash落桶一次,幾乎等於重新執行了一次所有元素的put。
所以如果你心目中有明確的Map 大小,設定時一定要考慮加載因子的存在。
建議你在知道你要存儲的容量的時候,直接這樣定義:
Map mapBest = new HashMap((int) ((float) 擬存的元素個數 / 0.75F + 1.0F));
這樣一次到位,雖然存在些資源浪費,但是比起重新擴容還是效率高很多
Map map = new HashMap(srcMap.size())這樣的寫法肯定是不對的,有25%的可能會遇上擴容。
Thrift裏的做法比較粗暴, Map map = new HashMap( 2* srcMap.size()), 直接兩倍又有點浪費空間。
Guava的做法則是加上如下計算
(int) ((float) expectedSize / 0.75F + 1.0F);
示例:
static class Demo {
int id;
String name;
public Demo(int id, String name) {
this.id = id;
this.name = name;
}
}
static List<Demo> demoList;
static {
demoList = new ArrayList();
for (int i = 0; i < 10000; i ++) {
demoList.add(new Demo(i, "test"));
}
}
public void test() {
Map map = new HashMap((int)(demoList.size() / 0.75f) + 1);
for (Demo demo : demoList) {
map.put(demo.id, demo.name);
}
}
減小加載因子
在構造函數裏,設定加載因子是0.5甚至0.25。
如果你的Map是一個長期存在而不是每次動態生成的,而裏面的key又是沒法預估的,那可以適當加大初始大小,同時減少加載因子,降低衝突的機率。畢竟如果是長期存在的map,浪費點數組大小不算啥,降低衝突概率,減少比較的次數更重要。