Arnold

基础知识梳理&HashMap扩容机制和加载因子

2019-07-31  本文已影响0人  诺诺诺诺诺诺诺诺诺
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;
    //... 省略
}

通过以上源码可以看到在源码中定义了一下几个常量:

很多人忽视的加载因子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,浪费点数组大小不算啥,降低冲突概率,减少比较的次数更重要。

上一篇下一篇

猜你喜欢

热点阅读