为什么阿里巴巴建议集合初始化时,指定集合容量大小?

集合是Java开发日常开发中经常会使用到的。在之前的一些文章中,我们介绍过一些关于使用集合类应该注意的事项,如《为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作》。

关于集合类,《阿里巴巴Java开发手册》中其实还有另外一个规定:


本文就来分析一下为什么会有如此建议?如果一定要设置初始容量的话,设置多少比较合适?

1

为什么要设置初始容量

我们先来写一段代码在JDK 1.7 (jdk1.7.0_79)下面来分别测试下,在不指定初始化容量和指定初始化容量的情况下性能情况如何。(jdk 8 结果会有所不同,我会在后面的文章中分析)

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));Map<Integer, Integer> map1 = new HashMap<>(aHundredMillion / 2);long s5 = System.currentTimeMillis();for (int i = 0; i < aHundredMillion; i++) {map1.put(i, i);}long s6 = System.currentTimeMillis();System.out.println("初始化容量5000000,耗时 :"+ (s6 - s5));Map<Integer, Integer> map2 = new HashMap<>(aHundredMillion);long s3 = System.currentTimeMillis();for (int i = 0; i < aHundredMillion; i++) {map2.put(i, i);}long s4 = System.currentTimeMillis();System.out.println("初始化容量为10000000,耗时 :"+ (s4 - s3));
}

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

输出结果:

未初始化容量,耗时 :14419
初始化容量5000000,耗时 :11916
初始化容量为10000000,耗时 :7984

从结果中,我们可以知道,在已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量可以有效的提高性能。

当然,以上结论也是有理论支撑的。我们HashMap中傻傻分不清楚的那些概念文章介绍过,HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中, threshold = loadFactor * capacity。

所以,如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。

从上面的代码示例中,我们还发现,同样是设置初始化容量,设置的数值不同也会影响性能,那么当我们已知HashMap中即将存放的KV个数的时候,容量设置成多少为好呢?

2

HashMap中容量的初始化

默认情况下,当我们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂作为初始化容量。

如以下示例代码:

Map<String, String> map = new HashMap<String, String>(1);
map.put("hahaha", "hollischuang");
Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : "+ capacity.invoke(map));

在jdk1.7中,初始化容量设置成1的时候,输出结果是2。在jdk1.8中,如果我们传入的初始化容量为1,实际上设置的结果也为1,上面代码输出结果为2的原因是代码中map.put(“hahaha”, “hollischuang”);导致了扩容,容量从1扩容到2。

那么,话题再说回来,当我们通过HashMap(int initialCapacity)设置初始容量的时候,HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率。(1->1、3->4、7->8、9->16)

在Jdk 1.7和Jdk 1.8中,HashMap初始化这个容量的时机不同。jdk1.8中,在调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。而在Jdk 1.7中,要等到第一次put操作时才进行这一操作。

不管是Jdk 1.7还是Jdk 1.8,计算初始化容量的算法其实是如出一辙的,主要代码如下:

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;

上面的代码挺有意思的,一个简单的容量初始化,Java的工程师也有很多考虑在里面。

上面的算法目的挺简单,就是:根据用户传入的容量值(代码中的cap),通过计算,得到第一个比他大的2的幂并返回。

聪明的读者们,如果让你设计这个算法你准备如何计算?如果你想到二进制的话,那就很简单了。举几个例子看一下:

请关注上面的几个例子中,蓝色字体部分的变化情况,或许你会发现些规律。5->8、9->16、19->32、37->64都是主要经过了两个阶段。

Step 1,5->7

Step 2,7->8

Step 1,9->15

Step 2,15->16

Step 1,19->31

Step 2,31->32

Step 1,37->63

Step 2,63->65

对应到以上代码中,Step 1:

n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;

对应到以上代码中,Step2:

return (n < 0) ?1 :(n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

Step 2 比较简单,就是做一下极限值的判断,然后把Step 1得到的数值+1。

Step 1 怎么理解呢?其实是对一个二进制数依次向右移位,然后与原值取或。其目的对于一个数字的二进制,从第一个不为0的位开始,把后面的所有位都设置成1。

随便拿一个二进制数,套一遍上面的公式就发现其目的了:

1100 1100 1100 >>>1 = 0110 0110 0110
1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0011 1011 1011
1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111
1111 1111 1111 >>>4 = 1111 1111 1111
1111 1111 1111 | 1111 1111 1111 = 1111 1111 1111

通过几次无符号右移按位或运算,我们把1100 1100 1100转换成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,这就是大于1100 1100 1100的第一个2的幂。

好了,我们现在解释清楚了Step 1和Step 2的代码。就是可以把一个数转化成第一个比他自身大的2的幂。(可以开始佩服Java的工程师们了,使用无符号右移和按位或运算大大提升了效率。)

但是还有一种特殊情况套用以上公式不行,这些数字就是2的幂自身。如果数字4 套用公式的话。得到的会是 8 :

Step 1: 
0100 >>>1 = 0010
0100 | 0010 = 0110
0110 >>>1 = 0011
0110 | 0011 = 0111
Step 2:
0111 + 0001 = 1000

为了解决这个问题,JDK的工程师把所有用户传进来的数在进行计算之前先-1,就是源码中的第一行:

int n = cap - 1;

至此,再来回过头看看这个设置初始容量的代码,目的是不是一目了然了:

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;

3

HashMap初始容量的合理值

当我们使用 HashMap(int initialCapacity) 来初始化容量的时候,jdk会默认帮我们计算一个相对合理的值当做初始容量。那么,是不是我们只需要把已知的HashMap中即将存放的元素个数直接传给initialCapacity就可以了呢?

关于这个值的设置,在《阿里巴巴Java开发手册》有以下建议:

这个值,并不是阿里巴巴的工程师原创的,在guava(21.0版本)中也使用的是这个值。

public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {return new HashMap<K, V>(capacity(expectedSize));
}
/**
* Returns a capacity that is sufficient to keep the map from being resized as long as it grows no
* larger than expectedSize and the load factor is ≥ its default (0.75).
*/
static int capacity(int expectedSize) {if (expectedSize < 3) {checkNonnegative(expectedSize, "expectedSize");return expectedSize + 1;}if (expectedSize < Ints.MAX_POWER_OF_TWO) {// This is the calculation used in JDK8 to resize when a putAll// happens; it seems to be the most conservative calculation we// can make.  0.75 is the default load factor.return (int) ((float) expectedSize / 0.75F + 1.0F);}return Integer.MAX_VALUE; // any large value
}

在return (int) ((float) expectedSize / 0.75F + 1.0F);上面有一行注释,说明了这个公式也不是guava原创,参考的是JDK8中putAll方法中的实现的。感兴趣的读者可以去看下putAll方法的实现,也是以上的这个公式。

虽然,当我们使用 HashMap(int initialCapacity) 来初始化容量的时候,jdk会默认帮我们计算一个相对合理的值当做初始容量。但是这个值并没有参考loadFactor的值。

也就是说,如果我们设置的默认值是7,经过Jdk处理之后,会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。

如果我们通过 expectedSize / 0.75F + 1.0F 计算,7/0.75 + 1 = 10 ,10经过Jdk处理之后,会被设置成16,这就大大的减少了扩容的几率。

当HashMap内部维护的哈希表的容量达到75%时(默认情况下),会触发rehash,而rehash的过程是比较耗费时间的。所以初始化容量要设置成expectedSize/0.75 + 1的话,可以有效的减少冲突也可以减小误差。

所以,我可以认为,当我们明确知道HashMap中元素的个数的时候,把默认容量设置成 expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

4

总结

当我们想要在代码中创建一个HashMap的时候,如果我们已知这个Map中即将存放的元素个数,给HashMap设置初始容量可以在一定程度上提升效率。

但是,JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂。原因在《全网把Map中的hash()分析的最透彻的文章,别无二家》介绍过,得到这个数字的算法其实是使用了使用无符号右移和按位或运算来提升效率。

但是,为了最大程度的避免扩容带来的性能消耗,我们建议可以把默认容量的数字设置成expectedSize / 0.75F + 1.0F 。在日常开发中,可以使用

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

来创建一个HashMap,计算的过程guava会帮我们完成。

但是,以上的操作是一种用内存换性能的做法,真正使用的时候,要考虑到内存的影响。

最后,留一个思考题:为什么JDK 8中,putAll方法采用了这个expectedSize / 0.75F + 1.0F公式,而put、构造函数等并没有默认使用这个公式呢?

【End】

老王给大家准备一份「Java最常见200+面试题全解析」,助力大家找到更好的工作,这份面试题包含的模块:

  • Java、JVM 最常见面试题解析

  • Spring、Spring MVC、MyBatis、Hibernate 面试题解析

  • MySQL、Redis 面试题解析

  • RabbitMQ、Kafka、Zookeeper 面试解析

  • 微服务 Spring Boot、Spring Cloud 面试解析

扫描下面二维码付费阅读

关注下方二维码,订阅更多精彩内容。

转发朋友圈,是对我最大的支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546662.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

十五、Python操作mysql数据库

利用Navicat Premium 15软件连接mysql数据库&#xff0c;新建testdb数据库&#xff0c;并添加2个表usertest和userinfo。 main.py #!/usr/bin/python3 # -*- coding: utf-8 -*- import reimport pymysql # 导入模块myConn pymysql.connect(host127.0.0.1, # 主机模块port33…

面试必备的分布式事物方案

四月初&#xff0c;去面试了本市的一家之前在做办公室无人货架的公司&#xff0c;虽然他们现在在面临着转型&#xff0c;但是对于我这种想从传统企业往互联网行业走的孩子来说&#xff0c;还是比较有吸引力的。在面试过程中就提到了分布式事物问题。我又一次在没有好好整理的问…

记住:永远不要在MySQL中使用UTF-8

原文地址:https://dwz.cn/QS4wLyjh最近我遇到了一个bug&#xff0c;我试着通过Rails在以“utf8”编码的MariaDB中保存一个UTF-8字符串&#xff0c;然后出现了一个离奇的错误&#xff1a;Incorrect string value: ‘\xF0\x9F\x98\x83 <…’ for column ‘summary’ at row 1我…

面试官:不使用synchronized和lock,如何实现一个线程安全的单例?

单例&#xff0c;大家肯定都不陌生&#xff0c;这是Java中很重要的一个设计模式。稍微了解一点单例的朋友也都知道实现单例是要考虑并发问题的&#xff0c;一般情况下&#xff0c;我们都会使用synchronized来保证线程安全。那么&#xff0c;如果有这样一道面试题&#xff1a;不…

Mac OS X中配置Apache

我使用的Mac OS X版本是10.8.2&#xff0c;Mac自带了Apache环境。 启动Apache设置虚拟主机启动Apache 打开“终端(terminal)”&#xff0c;输入 sudo apachectl -v&#xff0c;&#xff08;可能需要输入机器秘密&#xff09;。如下显示Apache的版本 接着输入 sudo apachectl st…

你真的理解零拷贝了吗?

作者&#xff1a;ksfzhaohui 来源&#xff1a;http://t.cn/ESALgwV前言从字面意思理解就是数据不需要来回的拷贝&#xff0c;大大提升了系统的性能&#xff1b;这个词我们也经常在java nio&#xff0c;netty&#xff0c;kafka&#xff0c;RocketMQ等框架中听到&#xff0c;经常…

一、华为鸿蒙开发HUAWEI DevEco Studio下载、安装与配置

一、HUAWEI DevEco Studio下载 https://developer.harmonyos.com/cn/develop 二、HUAWEI DevEco Studio安装 解压后&#xff0c;双击安装包。 打开启动 DevEco Studio 三、DevEco Studio配置 DevEco Studio开发环境需要依赖于网络环境&#xff0c;需要连接上…

从JDK中,我们能学到哪些设计模式?

作者&#xff1a;肥朝 来自&#xff1a;肥朝&#xff08;ID&#xff1a;feichao_java&#xff09;结构性模式&#xff1a;适配器模式&#xff1a;常用于将一个新接口适配旧接口肥朝小声逼逼&#xff1a;在我们业务代码中经常有新旧接口适配需求&#xff0c;可以采用该模式。桥…

二、华为鸿蒙开发DevEco Studio运行第一个Hello World工程

1.打开DevEco Studio,创建一个Empty Ability(Java)工程,工程类型:Application 2.按照下图,Tools->Device Manager打开设备管理

解析url

2019独角兽企业重金招聘Python工程师标准>>> #include <stdio.h> #include <string.h> #include <stdlib.h>// 解析url&#xff0c;作为示例&#xff0c;很多情况没考虑&#xff0c;比如说user,pass之类的 int parse_url(char *url, char **serve…

面试官:讲一下Jvm中如何判断对象的生死?

但凡问到 JVM&#xff08;Java 虚拟机&#xff09;通常有 99% 的概率一定会问&#xff0c;在 JVM 中如何判断一个对象的生死状态&#xff1f;判断对象的生死状态的算法有以下几个&#xff1a;1、引用计数器算法引用计算器判断对象是否存活的算法是这样的&#xff1a;给每一个对…

三、华为鸿蒙HarmonyOS应用开发HUAWEI DevEco Studio实现页面跳转

在上一节二、华为鸿蒙开发DevEco Studio运行第一个Hello Word工程 基础上进行下面步骤。 在Java UI框架中,提供了两种编写布局的方式:在XML中声明UI布局和在代码中创建布局。这两种方式创建出的布局没有本质差别,为了熟悉两种方式,我们将通过XML的方式编写第一个页面,通过…

MVVM架构~前台后台分离的思想与实践

返回目录 MVVM是一种架构思想&#xff0c;是一种解决问题的方式&#xff0c;对于一个项目&#xff0c;一个功能模块&#xff0c;你可以选择使用&#xff2d;&#xff36;&#xff36;&#xff2d;的架构来实现&#xff0c;而knockoutjs只是实现MVVM的一种工具&#xff0c;它是在…

Java中所有锁介绍

在读很多并发文章中&#xff0c;会提及各种各样锁如公平锁&#xff0c;乐观锁等等&#xff0c;这篇文章介绍各种锁的分类。介绍的内容如下&#xff1a;1.公平锁 / 非公平锁2.可重入锁 / 不可重入锁3.独享锁 / 共享锁4.互斥锁 / 读写锁5.乐观锁 / 悲观锁6.分段锁7.偏向锁 / 轻量…

python深拷贝,浅拷贝,赋值引用

1.在python中&#xff0c;对象赋值实际上是对象的引用。当创建一个对象&#xff0c;然后把它赋给另一个变量的时候&#xff0c;python并没有拷贝这个对象&#xff0c;而只是拷贝了这个对象的引用 &#xff08;1&#xff09;直接赋值,默认浅拷贝传递对象的引用而已,原始列表改变…

pjtool用到的数据库----oracle范畴

PL/SQL Developer 专门面向Oracle数据库存储程序单元的开发 PL/SQL&#xff1a;过程化SQL语言转载于:https://www.cnblogs.com/ejllen/p/3684890.html

如何让mysql索引更快一点

后端开发&#xff0c;公众号内容包括但不限于 python、mysql、数据结构和算法、网络协议、Linux。技术人怎能只有技术和代码&#xff0c;如果你对投资理财、保险&#xff0c;英语学习、读书写作有兴趣&#xff0c;都欢迎来公众号【谭某人】与我交流&#xff0c;你总会有些收获。…

将本地项目上传到码云(gitee)远程仓库

前提条件&#xff1a; 1、本地电脑上已经安装了 git客户端&#xff0c;未安装的&#xff0c;具体安装过程可以参考此安装链接&#xff1a;https://blog.csdn.net/ezreal_tao/article/details/81609883 2、用户已经在gitee码云上注册完成 具体操作步骤&#xff1a; 1、登录码云…

单线程的Redis为什么却能支撑高并发?

作者&#xff1a;Draveness原文链接&#xff1a;draveness.me/redis-io-multiplexing最近在看 UNIX 网络编程并研究了一下 Redis 的实现&#xff0c;感觉 Redis 的源代码十分适合阅读和分析&#xff0c;其中 I/O 多路复用&#xff08;mutiplexing&#xff09;部分的实现非常干净…

在Windows10上安装WSL使用binwalk命令

Windows 10 推出的WSL 功能可以协助我们直接使用binwalk 分析Windows 内的文件 一、WSL 准备 1、打开控制面板→应用→程序和功能→启动或关闭Windows 功能&#xff0c;打开“适用于Linux 的Windows 子系统”和“虚拟机平台”&#xff0c;随后需要重启。 2、打开Windows 10 自…