Java基礎知識梳理&HashMap擴容機制和加載因子

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,浪費點數組大小不算啥,降低衝突概率,減少比較的次數更重要。

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