HashMap中傻傻分不清楚的那些概念

简介: 很多人在通过阅读源码的方式学习Java,这是个很好的方式。而JDK的源码自然是首选。在JDK的众多类中,我觉得HashMap及其相关的类是设计的比较好的。很多人读过HashMap的代码,不知道你们有没有和我一样,觉得HashMap中关于容量相关的参数定义的太多了,傻傻分不清楚。

作者 | Hollis

很多人在通过阅读源码的方式学习Java,这是个很好的方式。而JDK的源码自然是首选。在JDK的众多类中,我觉得HashMap及其相关的类是设计的比较好的。很多人读过HashMap的代码,不知道你们有没有和我一样,觉得HashMap中关于容量相关的参数定义的太多了,傻傻分不清楚。

其实,这篇文章介绍的内容比较简单,只要认真的看看HashMap的原理还是可以理解的,单独写一篇文章的原因是因为我后面还有几篇关于HashMap源码分析的文章,这些概念不熟悉的话阅读后面的文章会很吃力。

先来看一下,HashMap中都定义了哪些成员变量。

image.png

上面是一张HashMap中主要的成员变量的图,其中有一个是我们本文主要关注的: sizeloadFactorthresholdDEFAULT_LOAD_FACTORDEFAULT_INITIAL_CAPACITY

我们先来简单解释一下这些参数的含义,然后再分析他们的作用。

HashMap类中有以下主要成员变量:

  • transient int size;

    • 记录了Map中KV对的个数
  • loadFactor

    • 装载印子,用来衡量HashMap满的程度。loadFactor的默认值为0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;)。
  • int threshold;

    • 临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
  • 除了以上这些重要成员变量外,HashMap中还有一个和他们紧密相关的概念:capacity

    • 容量,如果不指定,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;)

可能看完了你还是有点蒙,size和capacity之间有啥关系?为啥要定义这两个变量。loadFactor和threshold又是干啥的?

size 和 capacity

HashMap中的size和capacity之间的区别其实解释起来也挺简单的。我们知道,HashMap就像一个“桶”,那么capacity就是这个桶“当前”最多可以装多少元素,而size表示这个桶已经装了多少元素。来看下以下代码:

    Map<String, String> map = new HashMap<String, String>();
    map.put("hollis", "hollischuang");

    Class<?> mapType = map.getClass();
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));

    Field size = mapType.getDeclaredField("size");
    size.setAccessible(true);
    System.out.println("size : " + size.get(map));

我们定义了一个新的HashMap,并想其中put了一个元素,然后通过反射的方式打印capacity和size。输出结果为:capacity : 16、size : 1

默认情况下,一个HashMap的容量(capacity)是16,设计成16的好处我在《全网把Map中的hash()分析的最透彻的文章,别无二家。》中也简单介绍过,主要是可以使用按位与替代取模来提升hash的效率。

为什么我刚刚说capacity就是这个桶“当前”最多可以装多少元素呢?当前怎么理解呢。其实,HashMap是具有扩容机制的。在一个HashMap第一次初始化的时候,默认情况下他的容量是16,当达到扩容条件的时候,就需要进行扩容了,会从16扩容成32。

我们知道,HashMap的重载的构造函数中,有一个是支持传入initialCapacity的,那么我们尝试着设置一下,看结果如何。

    Map<String, String> map = new HashMap<String, String>(1);

    Class<?> mapType = map.getClass();
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));

    Map<String, String> map = new HashMap<String, String>(7);

    Class<?> mapType = map.getClass();
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));


    Map<String, String> map = new HashMap<String, String>(9);

    Class<?> mapType = map.getClass();
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));

分别执行以上3段代码,分别输出:capacity : 2、capacity : 8、capacity : 16。

也就是说,默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(1->1、7->8、9->16)

这里有一个小建议:在初始化HashMap的时候,应该尽量指定其大小。尤其是当你已知map中存放的元素个数时。(《阿里巴巴Java开发规约》)

loadFactor 和 threshold

前面我们提到过,HashMap有扩容机制,就是当达到扩容条件时会进行扩容,从16扩容到32、64、128…

那么,这个扩容条件指的是什么呢?

其实,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)时就会触发扩容。

验证代码如下:

    Map<String, String> map = new HashMap<>();
    map.put("hollis1", "hollischuang");
    map.put("hollis2", "hollischuang");
    map.put("hollis3", "hollischuang");
    map.put("hollis4", "hollischuang");
    map.put("hollis5", "hollischuang");
    map.put("hollis6", "hollischuang");
    map.put("hollis7", "hollischuang");
    map.put("hollis8", "hollischuang");
    map.put("hollis9", "hollischuang");
    map.put("hollis10", "hollischuang");
    map.put("hollis11", "hollischuang");
    map.put("hollis12", "hollischuang");
    Class<?> mapType = map.getClass();

    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));

    Field size = mapType.getDeclaredField("size");
    size.setAccessible(true);
    System.out.println("size : " + size.get(map));

    Field threshold = mapType.getDeclaredField("threshold");
    threshold.setAccessible(true);
    System.out.println("threshold : " + threshold.get(map));

    Field loadFactor = mapType.getDeclaredField("loadFactor");
    loadFactor.setAccessible(true);
    System.out.println("loadFactor : " + loadFactor.get(map));

    map.put("hollis13", "hollischuang");
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map));

    Field size = mapType.getDeclaredField("size");
    size.setAccessible(true);
    System.out.println("size : " + size.get(map));

    Field threshold = mapType.getDeclaredField("threshold");
    threshold.setAccessible(true);
    System.out.println("threshold : " + threshold.get(map));

    Field loadFactor = mapType.getDeclaredField("loadFactor");
    loadFactor.setAccessible(true);
    System.out.println("loadFactor : " + loadFactor.get(map));

输出结果:

capacity : 16
size : 12
threshold : 12
loadFactor : 0.75

capacity : 32
size : 13
threshold : 24
loadFactor : 0.75

当HashMap中的元素个数达到13的时候,capacity就从16扩容到32了。

HashMap中还提供了一个支持传入initialCapacity,loadFactor两个参数的方法,来初始化容量和装载因子。不过,一般不建议修改loadFactor的值。

总结

HashMap中size表示当前共有多少个KV对,capacity表示当前HashMap的容量是多少,默认值是16,每次扩容都是成倍的。loadFactor是装载因子,当Map中元素个数超过loadFactor* capacity的值时,会触发扩容。loadFactor* capacity可以用threshold表示。

PS:文中分析基于JDK1.8.0_73

来源 | HollisChuang's Blog

相关文章
|
Linux 开发工具 数据安全/隐私保护
嵌入式Linux开发环境搭建之十---阿里云服务器购买及Linux开发环境搭建
嵌入式Linux开发环境搭建之十---阿里云服务器购买及Linux开发环境搭建
360 0
ldap落地实战:svn集成ldap认证
上一篇文章我们介绍了LDAP的部署以及管理维护,那么如何接入LDAP实现账号统一认证呢?这篇文章将带你完成svn的接入验证 subversion集成openldap认证 系统环境:debian8.
2698 0
|
算法 Serverless
通过函数计算部署ComfyUI以实现一个AIGC图像生成系统
快来报名参与ComfyUI活动,体验一键部署模型,生成粘土风格作品赢取体脂秤。活动时间从即日起至2024年12月13日24:00:00,每个工作日限量50个,先到先得!访问活动页面了解更多详情。
463 55
|
12月前
|
传感器 数据采集 人工智能
可穿戴设备中的生物识别技术:连接人体与数据的桥梁
可穿戴设备中的生物识别技术:连接人体与数据的桥梁
256 7
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
368 1
|
人工智能 安全 Android开发
《鸿蒙系统AI优势:超越安卓,开启智能新篇》
鸿蒙系统在人工智能浪潮中展现出独特优势。其系统级原生智能简化开发,AI大模型接入提升智慧交互,微内核架构保障安全稳定,分布式架构实现多设备无缝协同,隐私保护措施全面,智能检测实时防护。相比安卓,鸿蒙在开发效率、智能性、安全性及用户体验上更具竞争力,为用户带来更智能、便捷、安全的使用体验。
1331 6
|
数据采集 监控 搜索推荐
用户画像构建:年度数据分析的用户视角
在数据驱动的时代,年度数据分析对企业战略规划和运营优化至关重要。本文从数据收集、预处理、分析、可视化到应用实践,全面探讨如何通过年度数据分析实现业务增长,助力企业精准决策。通过构建全面的数据源体系、清洗整合数据、洞察趋势、发现机会,并借助数据可视化工具,最终将数据转化为实际行动,持续优化企业运营。
|
物联网 vr&ar Android开发
移动应用与系统:探索现代移动技术的发展趋势####
【10月更文挑战第29天】 本文深入探讨了移动应用开发和移动操作系统的最新趋势,分析了它们对日常生活和行业的影响。随着智能手机的普及和技术的进步,移动应用和操作系统不断进化,以满足用户的需求和期望。文章将通过具体案例和数据分析,揭示这一领域的创新点和未来发展方向。 ####
|
敏捷开发 监控 数据可视化
一文带你了解:六款适合PC端的工时管理工具
在项目管理中,工时管理软件能实时反映项目各任务的进展情况。管理者可以通过查看员工在每个任务上的工时投入,判断任务是否按计划推进。若发现某个任务工时投入远超预期但进度缓慢,就可以及时介入调查原因,是遇到技术难题、资源不足还是人员协作问题等,进而采取相应措施加以解决,确保项目能按时交付。
|
SQL 负载均衡 安全
阿里云DTS踩坑经验分享系列|全量迁移加速方法指南
阿里云数据传输服务DTS是一个便捷、高效的数据迁移和数据同步服务。一般而言,一个完整的DTS数据迁移任务主要包括预检查、结构迁移,全量迁移,增量迁移等阶段,其中全量迁移会将源数据库的存量数据全部迁移到目标数据库。面对各种各样的用户场景, 本文将重点介绍如何使用阿里云DTS实现全量数据迁移加速,以缩短迁移时间,确保数据迁移的效率和稳定性。
1371 0