热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析JDK8HashMap源代码:put方法详解及capacity、size、loadFactor和红黑树转换阈值的设定原理

本文深入解析了JDK8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。

HashMap


一、HashMap允许key为null,value为null,但是key为null只可以为一个,因为放进去key = null的新键值对,再放进去的时候会更新key = null的value值


二、put(key, value)的过程

return putVal(hash(key), key, value, false, true);

0、创建了HashMap以后,其capacity为16(仅仅是DEFAULT_INITIAL_CAPACITY值为16但是容量并不为16,目前里面实际存储键值对的Node数组仍然为空)、loadFactor为0.75;size(为键值对的个数而不是桶被占用的个数)、threshold为0,在put第一个值的时候才new Node[16],threshold=16 * 0.75;

HashMap底层存储数据其实是一个其内部类Node implements Map.Entry的数组

1、key先进行hash运算

(1)如果key为null,返回0;

(2)如果不为null,执行hashcode运算

Ⅰ.如果为普通数据(字符串、数字),那么它的hashCode运算为:

(数字会自动装箱转为包装类,使用其重写的hashcode运算)

* 字符串对象内部存储为数组;* int h = 0* 循环计算:h = h*31 + 字符数组中的字符ASCII值

Ⅱ.如果为Java对象,一般需要重写hashCode方法,不重写的话会调用本地方法,这个方法是C语言实现的,但是返回值绝对不是对象的内存地址;

public native int hashCode();

(3)hashCode的返回值hash和hash自己做无符号右移16位后的值做&运算(高位补0)

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

2、将hash值,key和value传入putVal()方法中,开始做散列


  • 将hash值 & 容量size- 1(初始为16),映射到Node数组上,判断这个位置是否为null

    • 为null,说明这个位置没有创建过值,那么就创建一个Node(hash, k, v, next = null)的节点放进去,这里next代表链表中的下一个节点,然后被修改的次数modcount + 1(结构上被修改的次数,这里的被修改次数指的是删除和增加,不包括更新值),size + 1【注释:modcount 该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,】

      • 如果size超过了threshold(初始为12),就将创建一个新的Node(capacity × 2)的数组,然后将旧的Node数组的所有键值对放进去(放到新数组的过程单独在下面分析),让HashMap里面的Node数组成员变量指向这个新的Node对象,返回null(记住,插入新的值就是返回null,更新值时返回老的value)
      • size没超过,直接返回null
    • 不为null说明这个位置有值,那么比较老的Node的key和新需要插入的Node的key是否相等;

      • key相等,则将原来的value替换为新的,并将来的老的value返回

      • key不相等

        • 查看当前Node数组位置的节点是不是一个红黑树TreeNode节点(p instanceof TreeNode判断的),是的话就按照红黑树节点进行遍历更新或插入数据,如果是被修改则返回老的值,如果是被插入数据那么返回null并且modcount + 1,size + 1,之后判断是否需要扩容

        • 不是红黑树节点遍历该位置是否有链表

          • 有链表,循环遍历链表中的,是否有K相等但是V不等的,如果有就将V替换成新的Value并且返回老的值,如果没有就在最后位置插入新节点并且modCount + 1,size + 1,
            • 如果是插入节点,判断链表长度是否大于8,如果大于8就扩成红黑树结构
          • 无链表,说明Node数组这个位置就这一个节点,把新的节点插入进去并且modCount + 1,size + 1,

扩容时如何复制节点到新的Node[]数组:(暂不考虑红黑树情况)

循环遍历每一个桶


  • 如果只有一个节点,直接hash & newCapacity - 1确定新的桶位置(新的位置要不就是新数组的原下标号桶里,要不就是在oldCapacity + 原下标号的桶里,这个下面代码块部分会解释);
  • 如果不止一个节点,循环遍历每一个节点,先hash & oldCapacity(老数组的容量),这样是为了判断你的下标需不需要挪到新扩出来的桶去,举个例子:

容量16(16-1 = 1111),hash1 = 10001和hash2 = 00001的桶位置都在1里面,但是扩容以后,容量32(32-1 = 11111)因为首位数字不同所以就不会在一个桶了,这样hash & oldCapacity是为了判断你的最高那一位是不是1,如果是0那你不用挪位置,如果是1,那就要挪桶的位置,挪到oldCapacity + 当前下标的位置(最高位为1,做hash运算的时候肯定会变为1XXXX,就等于oldCap + 当前下标)

  • 挪的过程中就会进行分表,即把这个老的链表拆开,一部分留在新数组原下标号的桶里,一部分放到(oldCapacity + 当前下标)的桶里


三、hashmap一些值设定的原因

1、为什么数值要为2^n个

(1)当哈希表的桶的个数为2^n 的时候,它的下标最大为 2^n - 1 ,这个时候转为二进制就是111…11的结构,可以保证与key的hash值做&运算得到的值可以完美投影到其中一个桶上,这样就不担心是否会投影到桶之外再做另外的处理。

例如当前容量为16,key的hash值为10001,我们直接使用16 = 10000 & 10001 = 10000 = 16 >桶的最大下标15,这个时候就还需要再做其他处理甚至可能需要做二次hash,那么是很麻烦和浪费时间的;

(2)可以尽可能的减少碰撞次数,因为只要两个key的hash值不同,与111…111的&运算就一定不同,可以尽可能的避免碰撞

2、为什么加载因子是0.75

这是考虑了时间和空间效率得出的折中方案,如果为1的时候在扩容那么会增大碰撞率,如果考虑一下中间值0.5的时候再扩容那么会浪费很多空间,因此要取一个折中方案。那么是因为0.5 + 1/ 2 = 0.75吗,实际维基百科有一个介绍说:

对于开放定址法,加载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了加载因子为0.75,超过此值将resize散列表。

因此折衷方案来自于这里!

有的博客说是因为那个泊松分布,在我看来那个泊松分布是说在加载因子为0.75的前提下,可以保证我们的桶上链表节点的个数大于等于8的可能性几乎为0,这个更像是说明链表转红黑树的设定阈值为8的原因。

参考原文:https://blog.csdn.net/NYfor2017/article/details/105454097/?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2

3、为什么链表长度大于等于8的时候转为红黑树,小于等于6的时候转为链表

(1)依据泊松分布,加载因子为0.75的时候,链表中的个数为8的可能性为亿分之6(代码中注释所描述),所以正常情况下是不会出现这种状况的,但是如果出现了就说明是非正常情况,这个时候可能有很多的key碰撞在同一个桶中,这个时候继续用链表的话查询时间复杂度O(n)效率是很低的,而红黑树的O(log(n))则要远远优于链表,举个例子,1024的链表查询平均时间为512,而红黑树则仅仅为10。节点数小于8的时候链表的查询时间是很小的,没必要用红黑树,而且红黑树调整树结构也需要时间,节点数也为链表节点数的2倍,所以没必要使用。

总结:Ⅰ桶上节点数出现8的概率很低,出现了说明为非正常情况,因此要额外处理;Ⅱ而当大于等于8的时候继续使用链表查询效率会比较低,改用红黑树会更快(这是以空间换取时间的策略);Ⅲ节点数小于8的时候考虑时间和空间效率没必要使用红黑树;

参考原文:https://blog.csdn.net/qq_43519310/article/details/102887039

(2)那为什么6是树转链表的阈值:因为达到了8的时候如果一个桶上频繁的增删节点可能节点数会不停的在8和7变动,那么阈值设定为7就要不停的链表和树互转,这个时候耗费时间空间,所以给它一个缓冲空间设定阈值为6的时候再转链表,达到6的时候说明节点数在向减少的趋势发展,所以设定为6了;


推荐阅读
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 重要知识点有:函数参数默许值、盈余参数、扩大运算符、new.target属性、块级函数、箭头函数以及尾挪用优化《深切明白ES6》笔记目次函数的默许参数在ES5中,我们给函数传参数, ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 在处理大规模数据数组时,优化分页组件对于提高页面加载速度和用户体验至关重要。本文探讨了如何通过高效的分页策略,减少数据渲染的负担,提升应用性能。具体方法包括懒加载、虚拟滚动和数据预取等技术,这些技术能够显著降低内存占用和提升响应速度。通过实际案例分析,展示了这些优化措施的有效性和可行性。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 本文深入探讨了NoSQL数据库的四大主要类型:键值对存储、文档存储、列式存储和图数据库。NoSQL(Not Only SQL)是指一系列非关系型数据库系统,它们不依赖于固定模式的数据存储方式,能够灵活处理大规模、高并发的数据需求。键值对存储适用于简单的数据结构;文档存储支持复杂的数据对象;列式存储优化了大数据量的读写性能;而图数据库则擅长处理复杂的关系网络。每种类型的NoSQL数据库都有其独特的优势和应用场景,本文将详细分析它们的特点及应用实例。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 本地存储组件实现对IE低版本浏览器的兼容性支持 ... [详细]
  • 本文介绍了如何利用 `matplotlib` 库中的 `FuncAnimation` 类将 Python 中的动态图像保存为视频文件。通过详细解释 `FuncAnimation` 类的参数和方法,文章提供了多种实用技巧,帮助用户高效地生成高质量的动态图像视频。此外,还探讨了不同视频编码器的选择及其对输出文件质量的影响,为读者提供了全面的技术指导。 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
    技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
  • 本文详细介绍了在MySQL中如何高效利用EXPLAIN命令进行查询优化。通过实例解析和步骤说明,文章旨在帮助读者深入理解EXPLAIN命令的工作原理及其在性能调优中的应用,内容通俗易懂且结构清晰,适合各水平的数据库管理员和技术人员参考学习。 ... [详细]
  • 泰波那契数列与斐波那契数列类似,但其计算方法有所不同。本文详细解析了如何高效计算第 N 个泰波那契数,并提供了一种基于动态规划的优化算法。通过使用数组记录中间结果,避免了重复计算,显著提高了算法的执行效率。代码示例展示了具体的实现方法,帮助读者更好地理解和应用这一算法。 ... [详细]
  • 在C语言中,指针的高级应用及其实例分析具有重要意义。通过使用 `&` 符号可以获取变量的内存地址,而 `*` 符号则用于定义指针变量。例如,`int *p;` 定义了一个指向整型的指针变量 `p`。其中,`p` 代表指针变量本身,而 `*p` 则表示指针所指向的内存地址中的内容。此外,指针在不同函数中可以具有相同的变量名,但其作用域和生命周期会有所不同。指针的灵活运用能够有效提升程序的效率和可维护性。 ... [详细]
author-avatar
mobiledu2502923573
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有