设置HashMap的初始容量

HashMap在Java的使用中占据着很重要的地位,平时使用的时候,相信很多Java程序员都知道在定义HashMap的时候,给它设置一个初始容量,以便减少hashMap扩容(resize)带来的额外开销,比如像我同(zi)事(ji)的这段代码:

@Test
public void longLongAGo() {int count = 1000000;System.out.println("---------------- 不设置hashMap初始容量 ------------");long start = System.currentTimeMillis();HashMap<Integer, Object> map = new HashMap<>();for (int i = 0; i < count; i++) {map.put(i, UUID.randomUUID());}long end = System.currentTimeMillis();System.out.println("添加1000000个元素耗时:" + (end - start));System.out.println("---------------- 设置hashMap初始容量 -------------------");long start1 = System.currentTimeMillis();HashMap<Integer, Object> map1 = new HashMap<>(count);for (int i = 0; i < count; i++) {map1.put(i, UUID.randomUUID());}long end1 = System.currentTimeMillis();System.out.println("添加1000000个元素耗时:" + (end1 - start1));
}

我同事说他在初始化的时候设定了map的容量,不会在添加元素的过程中进行自动扩容了,大大提高了性能,从结果看确实如此!

所以,集合初始化时,指定集合初始值大小能提升性能。

然鹅,我抱着怀疑的态度,对比了设置初始容量和不设置初始容量时,hashMap的扩展次数,当设置初始容量为1000000时,容器并不是想象中的不扩容了,而是也扩容了1次:

@SneakyThrows
@Test
public void testing() {int count = 1000000;System.out.println("---------------- 初始化hashMap容量为1000000 ------------");int resizeCount = 0;HashMap<Integer, Object> map = new HashMap<>(count);Method capacityMethod = map.getClass().getDeclaredMethod("capacity");capacityMethod.setAccessible(true);int capacity = (int) capacityMethod.invoke(map);System.out.println("初始容量:" + capacity);for (int i = 0; i < count; i++) {map.put(i, UUID.randomUUID());int curCapacity = (int) capacityMethod.invoke(map);if (curCapacity > capacity) {System.out.println("当前容量:" + curCapacity);resizeCount++;capacity = curCapacity;}}System.out.println("hashMap扩容次数:" + resizeCount);System.out.println("---------------- 不初始化hashMap容量 -------------------");resizeCount = 0;HashMap<Integer, Object> map1 = new HashMap<>();Method capacityMethod1 = map1.getClass().getDeclaredMethod("capacity");capacityMethod1.setAccessible(true);int capacity1 = (int) capacityMethod1.invoke(map1);System.out.println("初始容量:" + capacity1);for (int i = 0; i < count; i++) {map1.put(i, UUID.randomUUID());int curCapacity = (int) capacityMethod1.invoke(map1);if (curCapacity > capacity1) {System.out.println("当前容量:" + curCapacity);resizeCount++;capacity1 = curCapacity;}}System.out.println("扩容次数:" + resizeCount);
}

由于我们无法直接调用hashMap的capacity()方法,因此使用反射来查看每添加一个元素,它的容量变化,以此来监测hashMap的扩容次数。

//使用反射,调用hashMap的capacity()方法
Method capacityMethod = map.getClass().getDeclaredMethod("capacity");
capacityMethod.setAccessible(true);
int capacity = (int) capacityMethod.invoke(map);

差点跑偏了,现在回到上面程序的执行结果:

---------------- 初始化hashMap容量为1000000 ------------
初始容量:1048576
当前容量:2097152
hashMap扩容次数:1
---------------- 不初始化hashMap容量 -------------------
初始容量:16
当前容量:32
当前容量:64
当前容量:128
当前容量:256
当前容量:512
当前容量:1024
当前容量:2048
当前容量:4096
当前容量:8192
当前容量:16384
当前容量:32768
当前容量:65536
当前容量:131072
当前容量:262144
当前容量:524288
当前容量:1048576
当前容量:2097152
扩容次数:17

通过运行结果发现:

设置了初始容量的hashMap,其初始容量并不是我指定的1000000,而是1048576(2^20)
hashMap的容量并不是固定不变的,当达到扩容条件时会进行扩容,从 16 扩容到 32、64、128…(Hash 会选择大于当前容量的第一个 2 的幂作为容量)
即使制定了初始容量,而且初始容量是1048576,当添加1000000个元素(1000000是小于1048576)时,hashMap依然会扩容1次
为什么会酱紫呢?带着上面的三个发现,来看一下HashMap的扩容机制。

HashMap的扩容机制
先看一下HashMap的几个成员变量:

DEFAULT_INITIAL_CAPACITY:默认初始容量是2^4=16
DEFAULT_LOAD_FACTOR:默认的装载系数是0.75,是用来衡量HashMap的容量满的程度的
transient int size:map中k,v对的数目
final float loadFactor:装载系数,默认值为0.75
int threshold:调整大小的下一个大小值(容量*装载系数)。当实际 KV 个数超过 threshold 时,HashMap 会将容量扩容
再来看一个方法capacity():

final int capacity() {return (table != null) ? table.length :(threshold > 0) ? threshold :DEFAULT_INITIAL_CAPACITY;
}

这是啥?前面不是已经定义了一个size变量了吗?

可以把capacity看成是HashMap这个桶的体积(这个体积是可以变大的),而size是这个桶当前装了多少东西。

桶的容量是由threshold定义的,而且默认容量是2的4次幂,也就是16,源码上是这样写的:

/*** The default initial capacity - MUST be a power of two.*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

1 << 4就是左移4位的意思,也就是2^4=16。

那么什么时候扩容呢?这个很容易就能够想到,我们向hashMap这个桶里put数据,当桶的k,v对的数目size快填满桶-逼近capacity时,这个桶将要扩容,像金箍棒一样!

前面的例子已经展示了,hashMap并不是等size到了capacity才扩容,而是在到达capacity的某个值时就扩容了,这个值就是threshold的时候,hashMap进行resize(),而这个,来看源码:

当size增长到大于threshold的时候,hashMap进行resize(),而threshold = loadFactor * capacity,这样就可以知道hashMap这个桶在什么时候自动扩大它的体积了。

真正的避免HashMap扩容
前面分析到,当size > threshold的时候,hashMap进行扩容,利用threshold = loadFactor * capacity这个公式,我们在初始化的时候就有方向了。

首先肯定不能直接设置成loadFactor * capacity,因为这个数有可能不是2的幂,HashMap规定的容器容量必须是2的幂,既然如此,我设置成大于loadFactor * capacity的第一个2的幂的数就行了,可以这样做:

int initCapacity = 1 + (int) (count / 0.75);
HashMap<Integer, Object> map = new HashMap<>(initCapacity);

1 + (int) (count / 0.75)这个公式来源于HashMap源码:

/*** Returns a power of two size for the given target capacity.*/
static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这一段代码真的是天外飞仙!其目的是:根据传入的容量值cap,通过一系列神仙操作计算,得到第一个比他大的 2 的幂并返回。

这些都是二进制的位操作,将数依次向右移位,然后和原值取或。可以随便找一个数代入代码中验证,结果就是第一个比它大的2的幂!

为什么这样做,或许就是因为 无符号右移 >>> 、或运算 | 就是快吧!

结果验证

计算容量的公式前面已经搞出来了,现在验证一下对不对:

@SneakyThrows
@Test
public void perfect() {int count = 1000000;int initCapacity = 1 + (int) (count / 0.75);HashMap<Integer, Object> map = new HashMap<>(initCapacity);Method capacityMethod = map.getClass().getDeclaredMethod("capacity");capacityMethod.setAccessible(true);int capacity = (int) capacityMethod.invoke(map);System.out.println("jdk hashMap default capacity:" + capacity);int resizeCount = 0;for (int i = 0; i < count; i++) {map.put(i, UUID.randomUUID());int curCapacity = (int) capacityMethod.invoke(map);if (curCapacity > capacity) {System.out.println("当前容量:" + curCapacity);resizeCount++;capacity = curCapacity;}}System.out.println("hashMap扩容次数:" + resizeCount);

运行结果:

扩容次数为0,perfect!

把initCapacity=1333334这个数代入到HashMap的tableSizeFor方法就能算出容量为2097152=2^21了!

不想计算初始化容量-仍有他途

Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。

Guava中有现成的初始化HashMap的方法,它不用我们计算initCapacity,测试一把看看。

先引入Guava包:

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>29.0-jre</version>
</dependency>

测试:

@SneakyThrows
@Test
public void perfectWithGuava() {int count = 1000000;HashMap<Integer, Object> map = Maps.newHashMapWithExpectedSize(count);Method capacityMethod = map.getClass().getDeclaredMethod("capacity");capacityMethod.setAccessible(true);int capacity = (int) capacityMethod.invoke(map);System.out.println("guava hashMap default capacity:" + capacity);int resizeCount = 0;for (int i = 0; i < count; i++) {map.put(i, UUID.randomUUID());int curCapacity = (int) capacityMethod.invoke(map);if (curCapacity > capacity) {System.out.println("当前容量:" + curCapacity);resizeCount++;capacity = curCapacity;}}System.out.println("hashMap扩容次数:" + resizeCount);
}

运行结果:

同样能使HashMap不用扩容!

瞅一下关键代码:

HashMap<Integer, Object> map = Maps.newHashMapWithExpectedSize(count);

我猜这个newHashMapWithExpectedSize(int)的源码中肯定也是按照类似于HashMap的return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;这种方法计算的,来看一下:

小结
设置了初始容量的hashMap,其真实初始容量并不一定是指定的数值,而是HashMap内部计算过的
hashMap的容量并不是固定不变的,当达到扩容条件时会进行扩容,从 16 扩容到 32、64、128…(Hash 会选择大于当前容量的第一个 2 的幂作为容量)
不要以为指定了初始容量,hashMap就不扩容了
避免hashMap扩容的方法是传入一个1 + (int) (count / 0.75)计算出的初始值
还可以使用Guava的newHashMapWithExpectedSize(int count)

避免HashMap扩容的正确姿势相关推荐

  1. HDFS——JN扩容的正确姿势

    [前言] 有一段时间没有更文了,一方面是之前准备的hudi系列由于一些细节还没研究得很清楚,暂时没有继续更新.另一方面,最近事情相当多,回家后收拾收拾就十一二点了,也就没有再进行总结输出了. 不过,最 ...

  2. 开发方向校招准备的正确姿势,机会留给有准备的人

    一.背景 马上就快到校招的时间了. 网上有很多分享面经的地方,也有一些博文分享作者的面试经历,尤其是大公司的面试经历. 大多数是分享具体的问题,而没有系统的总结出方法论.导致大家只不过是在刷题!仅此而 ...

  3. android javamail获取邮件太多太慢_结合 Spring 发送邮件的4种正确姿势,你知道几种?...

    Java程序猿阿谷:面试字节跳动三轮凉凉,内推4面终拿下抖音offer(Java后台研发)​zhuanlan.zhihu.com 一.前言 测试所使用的环境 测试使用的环境是企业主流的SSM 框架即 ...

  4. 玩转Ceph的正确姿势

    内容目录: Ceph 客户端 Ceph 服务端 总结 参考 玩转 Ceph 的正确姿势 本文先介绍 Ceph, 然后会聊到一些正确使用 Ceph 的姿势:在集群规模小的时候,Ceph 怎么玩都没问题: ...

  5. Ubuntu创建新用户的正确姿势

    作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步<Ubuntu 创建新用户的正确姿势>原文地址.更欢迎来我的小站看更多原创内容:godbmw.com,进行&q ...

  6. io在Linux,在Linux进行IO的正确姿势

    原标题:在Linux进行IO的正确姿势 很多C/C++程序虽然在做网络编程, 但大多用别人封装好的库, 对底层不甚了解, 感觉 IO 操作不是很简单吗? 我敢说, 大多数人进行 IO 的姿势都不对, ...

  7. 互联网大厂内推求职的正确姿势?

    作者 | 码农唐磊 来源 | 程序猿石头(ID:tangleithu) 背景 每个人的职业生涯基本上都离不开"投简历找工作"这件事(什么,你家里有矿?当我没说),那拿着简历找工作正 ...

  8. Android获取设备状态栏status bar高度的正确姿势

    Android获取设备状态栏高度的正确姿势 正确代码方式: int height = 0;int resourceId = getApplicationContext().getResources() ...

  9. 开发函数计算的正确姿势——支持 ES6 语法和 webpack 压缩

    为什么80%的码农都做不了架构师?>>>    首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算 ...

最新文章

  1. windows怎么用qt MinGW gcc编译c代码
  2. 《Python数据科学指南》——1.23 采用键排序
  3. 智能窗帘研究制作_基于51单片机智能窗帘的研究与设计
  4. 第三次学JAVA再学不好就吃翔(part57)--StringBuffer和String的相互转换
  5. CodeVS 1081 线段树练习 2
  6. [MyBatisPlus]模拟多数据源环境及测试
  7. 关于Tomcat与MySQL连接池问题的详解
  8. 浏览器兼容问题 透明度 position:fixed bootstrap
  9. mac利用vscode运行c语言程序,Mac下使用VScode编译配置C/C++程序详细图文教程
  10. IOT(10)--RTOS
  11. mysql查询优化之三:查询优化器提示(hint)
  12. 微信购物商城系统怎样吸引住客户,来转换为商城系统的粉丝?
  13. JUCE学习笔记07-自定义正弦振荡器类
  14. 人生记录 2020-12-31 - 2021-3-10
  15. Android 插件化原理(三),通过hook启动插件Activity,修改Resources,调用插件资源
  16. WSJ0数据集简单介绍
  17. 信用卡欺诈检测:2021 年顶级机器学习解决方案
  18. 博士申请 | 香港大学黄凯斌教授招收6G通信与机器学习方向全奖博士生
  19. 教程 | 10分钟成为简笔画达人 6(POP字体+简笔技法)
  20. Spring MVC过滤器-字符集过滤器(CharacterEncodingFilter)

热门文章

  1. 输入输出重定向与fopen函数——C语言进阶
  2. mysql建表 float_mysql建表以及列属性
  3. 使用tcgames电脑玩手机游戏助手玩手游常见问题第二期
  4. 集五福2019最全攻略
  5. Golang 解析Yaml格式
  6. php哪里接私活_看了这些程序员接私活的渠道,瞬间爆炸!
  7. 我的世界服务器无限铁傀儡,我的世界如何快速刷铁傀儡 铁傀儡速刷攻略
  8. ImageIO 读取图片
  9. 缴党费,收党费,就用银联党费通
  10. 矩阵转置 java_用Java转置矩阵