你给HashMap初始化了容量,却让性能变加更糟?

2023-10-12 11:10

本文主要是介绍你给HashMap初始化了容量,却让性能变加更糟?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

项目中,看到大家已经意识到初始化HashMap时给Map指定初始容量大小,甚是欣慰。但仔细一看,发现事情好像又有一些不对头。虽然指定了大小,却让性能变得更加糟糕了。

可能你也是如此,看了《阿里巴巴Java开发手册》感觉学到了很多,于是在实践中开始尝试给Map指定初始大小,并感觉自己写的代码高大上了一些。

的确,当你意识到指定初始化值时,已经比普通人更进了一步,但是如果这个值指定的不好,程序的性能反而不如默认值。

这篇文章就来从头到尾分析一下,读者多注意分析的方法和底层的原理实现。

阿里开发规范

我们先来看看阿里巴巴Java开发规范中是如何描述Map初始值大小这一规范的吧。

阿里巴巴《Java开发手册》第1章编程规范,第6节集合处理的第17条规定如下:

【推荐】集合初始化时,指定集合初始值大小。说明:HashMap 使用HashMap(int initialCapacity)初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。

正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为0.75,如果暂时无法确定初始值大小,请设置为16(即默认值)。

反例:HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,resize需要重建hash表。当放置的集合元素个数达千万级别时,不断扩容会严重影响性能。

通过上面的规约我们大概了解到几个信息:

  • 第一,HashMap的默认容量是16;
  • 第二,容量扩容与负载因子和存储元素个数有关;
  • 第三,设置初始值是为了减少扩容导致重建hash的性能影响。

可能你看完上述规约之后,就开始在代码中进行使用指定集合初始值的方式了,这很好。但稍有不慎,这中间却会出现很多的问题,下面我们就来看看实例。

你指定的初始值对吗?

直接上一段示例代码,并思考这段代码是否有问题:

Map<String, String> map = new HashMap<>(4);
map.put("username","Tom");
map.put("address","Bei Jing");
map.put("age","28");
map.put("phone","15800000000");
System.out.println(map);

类似的代码是不是很熟悉,写起来也很牛的样子。HashMap使用了4个值,就初始化4个大小。空间完全利用,而且又满足了阿里开发手册的规约?!

上述写法真的对吗?真的没问题吗?直接看代码可能看不出来问题,我们添加一些打印信息。

如何验证扩容

很多朋友可能也想验证HashMap到底在什么时候进行扩容的,但苦于没有思路或方法。这里提供一个简单的方式,就是基于反射获取并打印一下capacity的值。

还是上面的示例我们改造一下,向HashMap中添加数据时,打印对应的capacity和size这两个属性的值。

public class MapTest {public static void main(String[] args) {Map<String, String> map = new HashMap<>(4);map.put("username", "Tom");print(map);map.put("address", "Bei Jing");print(map);map.put("age", "28");print(map);map.put("phone", "15800000000");print(map);}public static void print(Map<String, String> map) {try {Class<?> mapType = map.getClass();Method capacity = mapType.getDeclaredMethod("capacity");capacity.setAccessible(true);System.out.println("capacity : " + capacity.invoke(map) + "    size : " + map.size());} catch (Exception e) {e.printStackTrace();}}
}

其中print方法通过反射机制,获取到Map中的capacity和size属性值,然后进行打印。在main方法中,每新增一个数据,就打印一下Map的容量。

打印结果如下:

capacity : 4    size : 1
capacity : 4    size : 2
capacity : 4    size : 3
capacity : 8    size : 4

发现什么?当第4个数据put进去之后,HashMap的容量发生了一次扩容。

想想最开始我们指定初始容量的目的是什么?不就是为了避免扩容带来的性能损失吗?现在反而导致了扩容。

现在,如果去掉指定的初始值,采用new HashMap<>()的方式,执行一下程序,打印结果如下:

capacity : 16    size : 1
capacity : 16    size : 2
capacity : 16    size : 3
capacity : 16    size : 4

发现默认值并没有扩容,理论上性能反而更高了。是不是很有意思?你是不是也走进了这个使用误区了?

分析一下原理

之所会出现上述问题,最主要的是我们忽视了总结规约中的第二条,就是扩容机制。

HashMap的扩容机制,就是当达到扩容条件时会进行扩容。扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity。其中,默认的负载因子为0.75。

套入公式我们来算一下,负载因子为0.75,示例中capacity的值为4,得出临界值为4 * 0.75 = 3。也就说,当实际的size超过3之后,就会触发扩容,而扩容是直接将HashMap的容量加倍。这跟我们打印的结果一致。

JDK7和JDK8的实现是一样的,关于实现源码的分析,我们不放在本篇文章中进行分析。大家知道基本的原理及试验效果即可。

HashMap初始化容量设置多少合适

经过上面的分析,我们已经看到隐含的问题了。这时不禁要问,HashMap初始化容量设置多少合适呢?是不是随意写一个比较大的数字就可以了呢?

这就需要我们了解当传入初始化容量时,HashMap是如何处理的了。

当我们使用HashMap(int initialCapacity)来初始化容量时,HashMap并不会使用传入的initialCapacity直接作为初识容量。

JDK会默认帮计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个大于等于用户传入的值的2的幂的数值。实现源码如下:

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;
}

也就是说,当创建HashMap时,传入了7,则初始化的容量为8;当传入了18,则初始化容量为32。

此时,我们得出第一条结论:设置初始容量时,采用2的n次方的数值,即使不这么设置,JDK也会帮忙取下一个最近的2的n次方的数值。

上面的值看似合理了,但对于最初的实例,我们已经发现并不是存多少数据就设置多少的初始容量。因为还要考虑到扩容。

根据扩容公式可得出,如果设置初始容量为8,则8乘以0.75,也就6个值。在存储小于等于6个值的时候,不会触发扩容。

那么是否可以通过一个公式来反推呢?对应值的计算方法如下:

return (int) ((float) expectedSize / 0.75F + 1.0F);

比如,计划向HashMap中放入7个元素,通过expectedSize/0.75F + 1.0F计算,7/0.75 + 1 = 10,10经过JDK处理之后,会被设置成16。

那么此时,16就是比较合理的值,而且能大大减少了扩容的几率。

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

其他相关知识

了解上述知识,最后再补充一些HashMap相关的知识点:

  • HashMap在new后并不会立即分配bucket数组;
  • HashMap的bucket数组大小是2的幂;
  • HashMap在put的元素数量大于Capacity * LoadFactor(默认16 * 0.75)时会进行扩容;
  • JDK8在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构,提高性能;
  • JDK8在resize时,通过巧妙的设计,减少了rehash的性能消耗。

小结

本篇文章介绍了使用HashMap中的一些误区,得出最大的结论可能就是不要因为对一个知识点一知半解而导致错误使用。同时,也介绍了一些分析方法和实现原理。

可能有朋友会问,要不要设置HashMap的初识值,这个值又设置成多少,真的有那么大影响吗?不一定有很大影响,但性能的优化和个人技能的累积,不正是由这一点点的改进和提升而获得的吗?


程序新视界

公众号“ 程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料

微信公众号:程序新视界

这篇关于你给HashMap初始化了容量,却让性能变加更糟?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/195439

相关文章

Java数组初始化的五种方式

《Java数组初始化的五种方式》数组是Java中最基础且常用的数据结构之一,其初始化方式多样且各具特点,本文详细讲解Java数组初始化的五种方式,分析其适用场景、优劣势对比及注意事项,帮助避免常见陷阱... 目录1. 静态初始化:简洁但固定代码示例核心特点适用场景注意事项2. 动态初始化:灵活但需手动管理代

QT进行CSV文件初始化与读写操作

《QT进行CSV文件初始化与读写操作》这篇文章主要为大家详细介绍了在QT环境中如何进行CSV文件的初始化、写入和读取操作,本文为大家整理了相关的操作的多种方法,希望对大家有所帮助... 目录前言一、CSV文件初始化二、CSV写入三、CSV读取四、QT 逐行读取csv文件五、Qt如何将数据保存成CSV文件前言

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

Spring组件初始化扩展点BeanPostProcessor的作用详解

《Spring组件初始化扩展点BeanPostProcessor的作用详解》本文通过实战案例和常见应用场景详细介绍了BeanPostProcessor的使用,并强调了其在Spring扩展中的重要性,感... 目录一、概述二、BeanPostProcessor的作用三、核心方法解析1、postProcessB

Golang中拼接字符串的6种方式性能对比

《Golang中拼接字符串的6种方式性能对比》golang的string类型是不可修改的,对于拼接字符串来说,本质上还是创建一个新的对象将数据放进去,主要有6种拼接方式,下面小编就来为大家详细讲讲吧... 目录拼接方式介绍性能对比测试代码测试结果源码分析golang的string类型是不可修改的,对于拼接字

mysql线上查询之前要性能调优的技巧及示例

《mysql线上查询之前要性能调优的技巧及示例》文章介绍了查询优化的几种方法,包括使用索引、避免不必要的列和行、有效的JOIN策略、子查询和派生表的优化、查询提示和优化器提示等,这些方法可以帮助提高数... 目录避免不必要的列和行使用有效的JOIN策略使用子查询和派生表时要小心使用查询提示和优化器提示其他常

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录