热门标签 | 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了;


推荐阅读
  • 本文详细介绍了Java中org.neo4j.helpers.collection.Iterators.single()方法的功能、使用场景及代码示例,帮助开发者更好地理解和应用该方法。 ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 本文深入探讨了 Java 中的 Serializable 接口,解释了其实现机制、用途及注意事项,帮助开发者更好地理解和使用序列化功能。 ... [详细]
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
  • 本文介绍了一款用于自动化部署 Linux 服务的 Bash 脚本。该脚本不仅涵盖了基本的文件复制和目录创建,还处理了系统服务的配置和启动,确保在多种 Linux 发行版上都能顺利运行。 ... [详细]
  • 本文详细介绍了Java中org.eclipse.ui.forms.widgets.ExpandableComposite类的addExpansionListener()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。这些示例来源于多个知名开源项目,具有很高的参考价值。 ... [详细]
  • 本文详细介绍了Akka中的BackoffSupervisor机制,探讨其在处理持久化失败和Actor重启时的应用。通过具体示例,展示了如何配置和使用BackoffSupervisor以实现更细粒度的异常处理。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • 本文详细介绍了 Apache Jena 库中的 Txn.executeWrite 方法,通过多个实际代码示例展示了其在不同场景下的应用,帮助开发者更好地理解和使用该方法。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
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社区 版权所有