你给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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

PR曲线——一个更敏感的性能评估工具

在不均衡数据集的情况下,精确率-召回率(Precision-Recall, PR)曲线是一种非常有用的工具,因为它提供了比传统的ROC曲线更准确的性能评估。以下是PR曲线在不均衡数据情况下的一些作用: 关注少数类:在不均衡数据集中,少数类的样本数量远少于多数类。PR曲线通过关注少数类(通常是正类)的性能来弥补这一点,因为它直接评估模型在识别正类方面的能力。 精确率与召回率的平衡:精确率(Pr

SQL2005 性能监视器计数器错误解决方法

【系统环境】 windows 2003 +sql2005 【问题状况】 用户在不正当删除SQL2005后会造成SQL2005 性能监视器计数器错误,如下图 【解决办法】 1、在 “开始” --> “运行”中输入 regedit,开启注册表编辑器,定位到 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVer

Linux性能分析工具合集

Linux性能分析工具合集 工具合集主要包含以下各种工具,对于了解Linux系统结构、网络结构、内核层次具有一定的帮助。 Linux Performance Observability ToolsLinux Static Performance ToolsLinux Performance Benchmark ToolsLinux Performance Tuning ToolsLinux