java中hashmap容量的初始化实现

 更新时间:2019年11月22日 08:32:41   作者:杨冠标  
这篇文章主要介绍了java中hashmap容量的初始化实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

在默认的情况下,HashMap的容量是16。但是如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。比如如果指定了3,则容量是4;如果指定了7,则容量是8;如果指定了9,则容量是16。

为什么要设置HashMap的初始化容量

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

下面我们通过具体的代码来了解下为什么会这么建议。

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

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。

从上面的打印结果中可以得到一个初步的结论:在已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量可以有效地提高性能。下面我们来简单分析一下原因。

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

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->1、3->4、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);

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Mysql中备份表的多种方法

    Mysql中备份表的多种方法

    本文给大家分享Mysql中备份表的四种方法,第一种方式是小表的备份,第二种是对整个数据库的备份与恢复,第三种是对某个数据表进行备份,每种方式给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2022-11-11
  • 解决mybatisplus的分页插件和条件构造器自定义SQL遇到的BUG

    解决mybatisplus的分页插件和条件构造器自定义SQL遇到的BUG

    这篇文章主要介绍了解决mybatisplus的分页插件和条件构造器自定义SQL遇到的BUG,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • Spring Security如何在Servlet中执行

    Spring Security如何在Servlet中执行

    这篇文章主要介绍了Spring Security如何在Servlet中执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • springboot使用GuavaCache做简单缓存处理的方法

    springboot使用GuavaCache做简单缓存处理的方法

    这篇文章主要介绍了springboot使用GuavaCache做简单缓存处理的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • java中对象调用成员变量与成员实例方法

    java中对象调用成员变量与成员实例方法

    在本篇文章里小编给各位分享的是关于java中对象调用成员变量与成员实例方法,需要的朋友们可以学习参考下。
    2020-02-02
  • 详解Spring 中 Bean 对象的存储和取出

    详解Spring 中 Bean 对象的存储和取出

    由于 Spring 拥有对象的管理权,所以我们也需要拥有较为高效的对象存储和取出的手段,下面我们来分别总结一下,对Spring 中 Bean 对象的存储和取出知识感兴趣的朋友跟随小编一起看看吧
    2022-11-11
  • java判断某个点是否在所画多边形/圆形内

    java判断某个点是否在所画多边形/圆形内

    这篇文章主要为大家详细介绍了java判断某个点是否在所画多边形或圆形内的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-05-05
  • Java 代码检查工具之PMD入门使用详细教程

    Java 代码检查工具之PMD入门使用详细教程

    这篇文章主要介绍了Java 代码检查工具之PMD入门使用详细教程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • restTemplate未设置连接数导致服务雪崩问题以及解决

    restTemplate未设置连接数导致服务雪崩问题以及解决

    面对线上问题,仔细分析原因,及时调整配置,能有效解决问题,本文详细描述了线上遇到流量突增引发的问题,通过查看代码和连接池信息,分析出问题的原因是连接池满了,连接池大小配置不足以应对大并发流量,通过调整连接池大小配置
    2024-10-10
  • Maven直接依赖、间接依赖、依赖冲突、依赖仲裁的实现

    Maven直接依赖、间接依赖、依赖冲突、依赖仲裁的实现

    本文主要介绍了Maven直接依赖、间接依赖、依赖冲突、依赖仲裁的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-09-09

最新评论