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


推荐阅读
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社区 版权所有