场景
Java开发手册中对于HashMap的推荐如下:
【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:
initialCapacity = (需要存储的元素个数 / 负载因子) + 1。
注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:
HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,
resize()方法总共会调用 8 次,反复重建哈希表和数据迁移。
当放置的集合元素个数达千万级时会影响程序性能。
注:
博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
通过查看HashMap的源码,先看下其主要的成员变量
1、transient int size;
记录了 Map 中 KV 对的个数。
2、final float loadFactor;
装载因子,用来衡量 HashMap 满的程度。
loadFactor 的默认值为 0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
3、int threshold; 临界值,当实际 KV 个数超过 threshold 时,HashMap 会将容量扩容,threshold =容量 * 加载因子。
4、除了以上成员变量,还有一个概念capacity,容量,如果不指定,默认容量是16。
其源码声明为
/**
* The default initial capacity - MUST be a power of two.
*/
//static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
size指当前已经有多少元素,capacity指最多可以装多少元素。
5、默认情况下 HashMap 的容量是 16,但是,如果用户通过构造函数指定了一个数字作为容量,
那么 Hash 会选择大于该数字的第一个 2 的幂作为容量。
在初始化 HashMap 的时候,应该尽量指定其大小。尤其是当你已知 map 中存放的元素个数时。(《阿里巴巴 Java 开发规约》)
HashMap 的扩容条件就是当 HashMap 中的元素个数(size)超过临界值(threshold)时就会自动扩容。
在 HashMap 中,threshold = loadFactor * capacity。loadFactor 是装载因子,
表示 HashMap 满的程度,默认值为 0.75f,设置成0.75 有一个好处,那就是 0.75 正好是 3/4,而 capacity 又是 2 的幂。
所以,两个数的乘积都是整数。对于一个默认的 HashMap 来说,默认情况下,当其 size 大于 12(16*0.75) 时就会触发扩容。
为了验证初始化容量后的性能要高于默认容量,编写以下测试代码
int count = 10000000;
Map<Integer, Integer> map1 = new HashMap<>();
try(Cost cost = new Cost()){
for (int i = 0; i < count; i++) {
map1.put(i,i);
}
}
Map<Integer, Integer> map2 = new HashMap<Integer, Integer>((int) (count / 0.75 +1));
try(Cost cost = new Cost()){
for (int i = 0; i < count; i++) {
map2.put(i,i);
}
}
注意这里的代码耗时统计的方式可以参考
Java实战-基于JDK的LRU算法实现、优雅的实现代码耗时统计(Spring AOP、AutoCloseable方式):
Java实战-基于JDK的LRU算法实现、优雅的实现代码耗时统计(Spring AOP、AutoCloseable方式)_霸道流氓气质的博客-CSDN博客
以上对比结果如下
从结果中可知,在已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量可以有效的提高性能。
如果我们没有设置初始容量大小,随着元素的不断增加,HashMap 会发生多次扩容,
而 HashMap 中的扩容机制决定了每次扩容都需要重建 hash 表,是非常影响性能的。