java中hashmap容量的初始化

HashMap使用HashMap(int initialCapacity)对集合进行初始化。算法

在默认的状况下,HashMap的容量是16。可是若是用户经过构造函数指定了一个数字做为容量,那么Hash会选择大于该数字的第一个2的幂做为容量。好比若是指定了3,则容量是4;若是指定了7,则容量是8;若是指定了9,则容量是16。函数

为何要设置HashMap的初始化容量性能

在《阿里巴巴Java开发手册》中,有一条开发建议是建议咱们设置HashMap的初始化容量。测试

下面咱们经过具体的代码来了解下为何会这么建议。spa

咱们先来写一段代码在JDK1.7的环境下运行,来分别测试下,在不指定初始化容量和指定初始化容量的状况下性能状况的不一样。code

public static void main(String[] args) {
    int aHundredMillion = 10000000;

    // 未初始化容量
    Map<Integer, Integer> map = new HashMap<>();
    long s1 = System.currentTimeMillis();
    for (int i = 0; i < aHundredMillion; i++) {
        map.put(i, i);
    }
    long s2 = System.currentTimeMillis();
    System.out.println("未初始化容量,耗时: " + (s2 - s1)); // 14322

    // 初始化容量为50000000
    Map<Integer, Integer> map1 = new HashMap<>(aHundredMillion / 2);
    long s3 = System.currentTimeMillis();
    for (int i = 0; i < aHundredMillion; i++) {
        map1.put(i, i);
    }
    long s4 = System.currentTimeMillis();
    System.out.println("初始化容量5000000,耗时: " + (s4 - s3)); // 11819

    // 初始化容量为100000000
    Map<Integer, Integer> map2 = new HashMap<>(aHundredMillion);
    long s5 = System.currentTimeMillis();
    for (int i = 0; i < aHundredMillion; i++) {
        map2.put(i, i);
    }
    long s6 = System.currentTimeMillis();
    System.out.println("初始化容量为10000000,耗时: " + (s6 - s5)); // 7978
}

从以上的代码不难理解,咱们建立了3个HashMap,分别使用默认的容量(16)、使用元素个数的一半(5千万)做为初始容量和使用元素个数(一亿)做为初始容量进行初始化,而后分别向其中put一亿个KV。blog

从上面的打印结果中能够获得一个初步的结论:在已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量能够有效地提升性能。下面咱们来简单分析一下缘由。内存

咱们知道,HashMap是有扩容机制的。所谓的扩容机制,指的是当达到扩容条件的时候,HashMap就会自动进行扩容。而HashMap的扩容条件就是当HashMap中的元素个数(Size)超过临界值(Threshold)的状况下就会自动扩容。ci

threshold = loadFactor * capacity

在元素个数超过临界值的状况下,随着元素的不断增长,HashMap就会发生扩容,而HashMap中的扩容机制决定了每次扩容都须要重建hash表,这一操做须要消耗大量资源,是很是影响性能的。所以,若是咱们没有设置初始的容量大小,HashMap就可能会不断发生扩容,也就使得程序的性能下降了。资源

另外,在上面的代码中咱们会发现,一样是设置了初始化容量,设置的数值不一样也会影响性能,那么当咱们已知HashMap中即将存放的KV个数的时候,容量的设置就成了一个问题。

HashMap中容量的初始化

开头提到,在默认的状况下,当咱们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂做为初始化容量。

Map<String, String> map = new HashMap<>(1);
map.put("huangq", "yanggb");

Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map)); // 2

当初始化的容量设置成1的时候,经过反射取出来的capacity倒是2。在JDK1.8中,若是咱们传入的初始化容量为1,实际上设置的结果也是1。上面的代码打印的结果为2的缘由,是代码中给map塞入值的操做致使了扩容,容量从1扩容到了2。事实上,在JDK1.7和JDK1.8中,HashMap初始化容量(capacity)的时机不一样。在JDK1.8中,调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。而在JDK1.7中,要等到第一次put操做时才进行这一操做。

所以,当咱们经过HashMap(int initialCapacity)设置初始容量的时候,HashMap并不必定会直接采用咱们传入的数值,而是通过计算,获得一个新值,目的是提升hash的效率。好比1->一、3->四、7->8和9->16。

HashMap中初始容量的合理值

经过上面的分析咱们能够知道,当咱们使用HashMap(int initialCapacity)来初始化容量的时候,JDK会默认帮咱们计算一个相对合理的值当作初始容量。那么,是否是咱们只须要把已知的HashMap中即将存放的元素个数直接传给initialCapacity就能够了呢?

initialCapacity = (须要存储的元素个数 / 负载因子) + 1

这里的负载因子就是loaderFactor,默认值为0.75。

initialCapacity = expectedSize / 0.75F + 1.0F

上面这个公式是《阿里巴巴Java开发手册》中的一个建议,在Guava中也是提供了相同的算法,更甚之,这个算法其实是JDK8中putAll()方法的实现。这是公式的得出是由于,当HashMap内部维护的哈希表的容量达到75%时(默认状况下),就会触发rehash(重建hash表)操做。而rehash的过程是比较耗费时间的。因此初始化容量要设置成expectedSize/0.75 + 1的话,能够有效地减小冲突,也能够减少偏差。

总结

当咱们想要在代码中建立一个HashMap的时候,若是咱们已知这个Map中即将存放的元素个数,给HashMap设置初始容量能够在必定程度上提高效率。

可是,JDK并不会直接拿用户传进来的数字当作默认容量,而是会进行一番运算,最终获得一个2的幂。而为了最大程度地避免扩容带来的性能消耗,一般是建议能够把默认容量的数字设置成expectedSize / 0.75F + 1.0F。

在平常开发中,可使用Guava提供的一个方法来建立一个HashMap,计算的过程Guava会帮咱们完成。

Map<String, String> map = Maps.newHashMapWithExpectedSize(10);

最后要说的一点是,这种算法其实是一种使用内存换取性能的作法,在真正的应用场景中要考虑到内存的影响。

 

"当你认真喜欢一我的的时候,你的全世界都是她。"

相关文章
相关标签/搜索