源码-JDK1.8HashMap扩容方法resize()解析

深入理解HashMap(三)resize方法解析 前言 上一篇讲了put方法,在put方法的最后,我们需要检查容量是否超过阈值,,如果越界需要进行扩容。resize方法就是对hashmap进行扩容。那么hashmap的扩容需要哪些步骤呢? 假如我是设计的作者,我会先创建一个更大容量的数组,然后遍历原table,重新计算所有的节点的hash值对应的下标,然后将节点转移到新table中。那么这其中存在这几个问题: 什么时候进行resize操作? 扩... 阅读详情

1、准备知识

  • HashMap的底层数据结构

    Java语言的基本数据结构可以分为两种,一种是数组,另一种的模拟指针/引用,Java语言中涉及到的数据结构都是这两种的扩充。JDK1.8之前HashMap是数组+链表结合的链表散列。JDK1.8在解决哈希冲突上发生了变化,当链表长度大于阈值/默认8的时候,会将链表转化为红黑树,减少搜索时间。

在这里插入图片描述

  • hash算法

    我们希望HashMap的元素位置尽量分散,最好是每个位置只有一个元素,这样用hash算法求得该位置后可以直接返回结果,不用再遍历链表/红黑树。

    HashMap通过Key的hashCode经过扰动函数(就是hash方法)处理后得到hash值,然后在公式(n-1)& hash (/n是数组长度)判断当前元素的位置是否已存在元素,若已存在则判断新加入的元素和已存在的是否相同,相同则覆盖,不相同则拉链法解决冲突。

    //扰动函数 == hash方法
    /*JDK1.7
    此函数可确保仅在以下方面不同的hashCode每个位位置的恒定倍数有界碰撞次数(默认负荷系数下约为8)。
    */
    static int hash(int h){
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
     
    /*JDK1.8的hash方法比JDK1.7的更加简化,JDK1.7的扰动需要4次,JDK1.8的仅需1次*/
    static int hash(Object key){
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    • 拉链法:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。下次查询的时候,需要遍历这个链表找到元素,减慢了查询效率。

    • ->所有,在存储大规模的数据时,预先指定HashMap的大小为2的幂次方

      int capacity = 1;
      while(capacity < initialCapacity){
          capacity <<= 1;//扩大两倍
      }
      

    2、HashMap的resize()方法介绍

    • 当HashMap中的元素增多时,发生哈希冲突的几率就越来越高,这时候为了提高查询的效率,需要对数组大小进行扩容。

    • 当数组的元素规模 > 数组大小 * loadFactory时进行扩容,loadFactory默认是0.75

      ... map = new HashMap<>(c, loadFactor);
          //HashMap提供了以上有参构造方法
          //initialCapacity是HashMap数组的初始容量
          //loadFactor是加载因子
      
    • 什么时候进行resize?1、初始化table的时候 2、数组元素size超出threshold = map.size() * loadFactory

      直接扩容 原数组的2倍大小

    • 节点在转移的过程中是一个个节点复制还是一串一串的转移?从源码中我们可以看出,扩容时是先找到拆分后处于同一个桶的节点,将这些节点连接好,然后把头节点存入桶中即可

    • 话不多说,上源码

    • final Node<K,V>[] resize() {
              Node<K,V>[] oldTab = table;
              int oldCap = (oldTab == null) ? 0 : oldTab.length;
              int oldThr = threshold;
              int newCap, newThr = 0;
              //如果原table不为空
              if (oldCap > 0) {
              	//如果原容量已经达到最大容量了,无法进行扩容,直接返回
                  if (oldCap >= MAXIMUM_CAPACITY) {
                      threshold = Integer.MAX_VALUE;
                      return oldTab;
                  }
                  //设置新容量为旧容量的两倍
                  else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                           oldCap >= DEFAULT_INITIAL_CAPACITY)
                      //阈值也变为原来的两倍
                      newThr = oldThr << 1; // double threshold
              }
              /**
              * 从构造方法我们可以知道
              * 如果没有指定initialCapacity, 则不会给threshold赋值, 该值被初始化为0
          	* 如果指定了initialCapacity, 该值被初始化成大于initialCapacity的最小的2的次幂
      		* 这里这种情况指的是原table为空,并且在初始化的时候指定了容量,
      		* 则用threshold作为table的实际大小
      		*/
              else if (oldThr > 0) // initial capacity was placed in threshold
                  newCap = oldThr;
              //构造方法中没有指定容量,则使用默认值
              else {               // zero initial threshold signifies using defaults
                  newCap = DEFAULT_INITIAL_CAPACITY;
                  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
              }
              // 计算指定了initialCapacity情况下的新的 threshold
              if (newThr == 0) {
                  float ft = (float)newCap * loadFactor;
                  newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                            (int)ft : Integer.MAX_VALUE);
              }
              threshold = newThr;
      
      
          /**从以上操作我们知道, 初始化HashMap时, 
          *  如果构造函数没有指定initialCapacity, 则table大小为16
          *  如果构造函数指定了initialCapacity, 则table大小为threshold,
          *  即大于指定initialCapacity的最小的2的整数次幂
          
          *  从下面开始, 初始化table或者扩容, 实际上都是通过新建一个table来完成
          */ 
      
              @SuppressWarnings({"rawtypes","unchecked"})
              Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
              table = newTab;
              if (oldTab != null) {
                  for (int j = 0; j < oldCap; ++j) {
                      Node<K,V> e;
                      if ((e = oldTab[j]) != null) {
                        /** 这里注意, table中存放的只是Node的引用,这里将oldTab[j]=null只是清除旧表的引用, 
                         * 但是真正的node节点还在, 只是现在由e指向它
                         */
                          oldTab[j] = null;
                          //桶中只有一个节点,直接放入新桶中
                          if (e.next == null)
                              newTab[e.hash & (newCap - 1)] = e;
                          //桶中为红黑树,则对树进行拆分,对树的操作有机会再讲
                          else if (e instanceof TreeNode)
                              ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                          //桶中为链表,对链表进行拆分
                          else { // preserve order
                          	//下面为对链表的拆分,我们单独来讲一下。
                              Node<K,V> loHead = null, loTail = null;
                              Node<K,V> hiHead = null, hiTail = null;
                              Node<K,V> next;
                              do {
                                  next = e.next;
                                  if ((e.hash & oldCap) == 0) {
                                      if (loTail == null)
                                          loHead = e;
                                      else
                                          loTail.next = e;
                                      loTail = e;
                                  }
                                  else {
                                      if (hiTail == null)
                                          hiHead = e;
                                      else
                                          hiTail.next = e;
                                      hiTail = e;
                                  }
                              } while ((e = next) != null);
                              if (loTail != null) {
                                  loTail.next = null;
                                  newTab[j] = loHead;
                              }
                              if (hiTail != null) {
                                  hiTail.next = null;
                                  newTab[j + oldCap] = hiHead;
                              }
                          }
                      }
                  }
              }
              return newTab;
          }
      
      
      • resize()操作之链表拆分

        /**
        这里定义了四个节点:loHead, loTail ,hiHead , hiTail,两个头节点两个尾节点
        选择出扩容后在同一个桶中的节点,直接将节点连接好头节点入桶即可。
        **/
            HashMap.Node<K,V> loHead = null, loTail = null;
            HashMap.Node<K,V> hiHead = null, hiTail = null;
            HashMap.Node<K,V> next;
            	//遍历该桶
                do {
                next = e.next;
                //找出拆分后仍处在同一个桶中的节点
                if ((e.hash & oldCap) == 0) {
                    if (loTail == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                }
                else {
                    if (hiTail == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                }
            } while ((e = next) != null);
                if (loTail != null) {
                loTail.next = null;
                newTab[j] = loHead;
            }
                if (hiTail != null) {
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
            }
        

        img

HashMap的扩容机制:JDK1.7,JDK1.8 参考文章: http://www.importnew.com/20386.html https://blog.csdn.net/z69183787/article/details/64920074?locationNum=15&amp;fps=1 以前看HashMap的时候,resize扩容部分没有仔细看,今天挤出时间,看之。 1. 确定hash桶数组的索引位置 我们当然希望这个Hash... 阅读详情

相关推荐

HashMap扩容机制

HashMap扩容的核心是“扩容触发→容量计算→元素迁移”,JDK1.7到1.8的升级,本质是扩容效率和并发安全性的优化:JDK1.7:先扩容后插入,逐个元素重算下标、头插法迁移,低效且有死循环风险,无红黑树适配;JDK1.8:先插入后扩容,高低位整体迁移(核心优化),尾插法避免倒置,支持红黑树拆分与退化,高效且无死循环;核心记忆点:JDK1.8扩容关键是拆分高低位,低位留原下标,高位加旧容量,结合源码理解更透彻。

ysct_999的博客 4084

JDK1.8中HashMap的resize()方法详解

resize()方法的代码比较长,我们先用文字来总结一下该方法具体做了什么。首先记录当前数组信息,当前数组、数组长度还有扩容阈值。接着就到了一个if-elseif-else的代码块,这些就是用来判断当前是进行初始化操作还是扩容操作,如果是扩容操作则需要进行双倍扩容,如果是初始化数组则需要设置数组容量。如果是扩容操作或者初始化的时候用户指定了初始容量,则要⽤新数组的大小重写计算扩容阈值重新生成一个数组(无论是扩容操作还是初始化操作都需要)。

frozenpenguin的博客 1885

HashMap的扩容机制------resize()

一:首先要知道HashMap什么时候扩容 当元素向HashMap容器中添加元素的时候,会判断当前元素的个数,如果当前元素的个数大于等于阈值时,即当前数组table的长度*加载因子就要进行自动扩容。 由于HashMap的底层数据结构是“链表散列”,即数组和链表的组合,而数组是无法自动扩容的,所以只能是换一个更大的数组去装填以前的元素和将要添加的新元素 分析resize()方法的源代码可以发现:在jd...

IM_MESSI的博客 1万+

JDK1.8中HashMap源码解析

目录一、红黑树的理解1.1、 2-3树1.1.1、 2-3的理解1.1.2、 如何生成一个2-3树的演示示例1.2、 红黑树1.2.1、2-3树转换为红黑树(采用左倾红黑树的方式)1.2.2、红黑树的理解二、HashMap的源码概述 一、红黑树的理解 1.1、 2-3树 1.1.1、 2-3的理解 红黑树是一种自平衡的二叉树,它可以避免二分搜索树在极端的情况下蜕化成链表的情况。那么什么是红黑树呢?要想便于了解红黑树,我们先了解一下跟它息息相关的2-3树。 2-3树是一种绝对平衡的多叉树,在这棵树中,

小志的博客 2257

jdk8之HashMap resize方法详解(深入讲解为什么1.8中扩容后的元素新位置为原位置+原数组长度)

缘起 在此之前,笔者对HashMap的工作原理进行过简单的解析, jdk8中的HashMap相对于jdk7有比较大的更新,本文主要是对改动后的resize()方法进行详细解析。看本文需要一定的HashMap基础,如果没有基础的同学建议先看笔者的另一篇文章 resize()方法的作用 resize()方法会在HashMap的键值对达到“阈值”后进行数组扩容,而扩容时会调用resize()方法,此外,...

诺浅的专栏 1万+

YOLOv8损失函数升级:从CIoU到Wise-IoU v3的实战调优与避坑指南

本文详细介绍了如何将YOLOv8的损失函数从CIoU升级到Wise-IoU v3,以提升目标检测性能,特别是在小目标检测和样本不均衡场景下的表现。通过实战调优步骤和避坑指南,帮助开发者有效应用Wise-IoU v3的动态聚焦机制和单调性控制,显著提升模型精度和训练稳定性。

weixin_33737774的博客 590

JDK1.8 HashMap中resize()方法解释

JDK1.8 HashMap中resize()方法解析 * 初始化或者是将table大小加倍。如果为空,则按threshold分配空间,否则,加倍后,每个容器中的元素在新table中要么呆在原索引处,要么有一个2的次幂的位移 * @return the table */ final Node[] resize() { Node[] oldTab

journey_fan的专栏 3347

PCB设计AD规则设置(按照嘉立创设置)

按照嘉立创的相关加工能力进行设置: 1.Hole Size(钻孔孔径) 嘉立创要求钻孔孔径在0.2-6.3mm,并给了公差(如下图给出了钻孔孔径和公差) 规则设置位置:Manufacturing->Hole Size,这里就改一下数值,最小设置为0.2mm,最大设置为6.3mm 2.Width(线宽) 嘉立创要求线宽多层板双层板最小为3.5mil,单双层板最小为5mil(如下图给出了线宽) 规则设置位置:Routing->Width,我目前只画过双层板,按我的设置如下,最小的线宽设置为10

subtitle_的博客 4万+

HashMap(jdk1.8) 之 resize() 扩容

1、为什么要扩容 为了减少hash碰撞,提高hashMap性能 (1)达到阈(yu)值扩容 (2)链表长度大于8,数组长度小于64 2、怎么扩容 代码如下: final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //老的容器长度 int oldCap = (oldTab == null) ? 0 : oldTab.length;...

zhang2383906154的博客 474

Libero SoC v2021.1离线安装与IP核配置全攻略

本文详细介绍了Libero SoC v2021.1在离线环境下的完整安装与配置流程。核心内容包括如何选择正确的离线安装包、获取并配置许可证、安装与挂载MegaVault IP库,以及将ModelSim仿真器切换至免费SE模式的关键步骤。文章旨在帮助内网开发者避开常见陷阱,顺利完成FPGA开发环境的搭建。

weixin_29279101的博客 406

手把手教你搭建硬件在回路(HIL)仿真系统:从Simulink建模到实时测试

本文详细介绍了如何从零开始搭建硬件在回路(HIL)仿真系统,涵盖Simulink建模、实时仿真、代码生成及调试等关键步骤。通过具体配置示例和优化技巧,帮助开发者解决复杂控制系统验证中的技术难题,提升半实物仿真的效率和可靠性。

weixin_30952535的博客 445

JDK 1.8 HashMap的resize分析

JDK 1.8 HashMap的resize分析 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 扩容前table的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 扩容前map的阈值 int oldThr = threshold; // 扩容后table的长度,map的阈值 int newCa

ty649128265的博客 398

QT快速配置MSVC2017编译环境(3分钟极简指南)

本文提供了一份3分钟极简指南,帮助开发者快速配置QT的MSVC2017编译环境。通过断网安装、精准勾选组件和配置QT Creator编译器路径等步骤,大幅提升QT开发效率。MSVC2017作为Windows平台高性能C++编译器,特别适合处理大型QT项目,编译速度优势明显。

weixin_30399821的博客 503

JDK1.8---HashMap的resize()扩容方法源码详解

JDK1.8---HashMap的resize()扩容方法源码详解resize()方法触发时机下面开始根据源码介绍HashMap的扩容原理下面进行分步分析扩容 resize()方法触发时机 首先总结一下JDK1.8的HashMap都在什么时候触发resize()方法,根据阅读源码总结了三个时机触发扩容,这里只做介绍,后面根据源码详细分析 HashMap是由数组+链表+红黑树构成的,数组就称之为桶了...

qq_36856024的博客 4282

基于MATLAB的井筒多相流模拟

井筒多相流的模拟可以基于不同的数学模型和计算方法。其中,最常用的模型之一是多相流模型,它基于质量守恒、动量守恒和能量守恒等原理,描述了流体在井筒中的流动行为。对于井筒多相流的模拟和分析可以帮助我们理解和优化油气田开发过程中的流体行为,从而提高产量和效率。在本文中,我们将介绍如何使用MATLAB进行井筒多相流的模拟,并提供相应的源代码。需要注意的是,以上示例是一个简化的模拟,仅用于演示如何使用MATLAB进行井筒多相流模拟。实际应用中,我们需要考虑更多的物理过程和参数,并使用更复杂的数值方法和模型进行模拟。

AtdiHtml的博客 706

华为OD机试 - 小扇和小船的数字游戏 - 二进制(Python/JS/C/C++ 2024 C卷 200分)

二进制表示:任何数 n 的二进制表示中,要找到下一个有相同数量的1的数,我们需要在最低位的0后面的最低位的1进行操作。右侧交换法则:找到右侧的第一个0,位于第一个1的左边,将这个0变成1(提升这个位),并将这个1变成0(下移这个位)。调整尾部1:为了得到最小的数,将所有低于已交换的1的位重置,使得所有1尽可能靠右。

学Java,找哪吒 1161

Java学习笔记——HashMap底层源码分析

目录源码内一些参数介绍实例化(声明一个HashMap对象)注意事项添加键值对hash(key)源码分析putVaule()源码分析resize()源码分析扩容后重新计算键值对在数组中的位置JDK1.7与JDK1.8的区别插入值put扩容resize() 源码内一些参数介绍 capacity:HashMap的容量,即Node[]数组的size,HashMap会强制的将容量设置为2的幂,这样在对key取hash值的时候可以直接使用位运算来计算hash值。(比如容量为16时,只取key对象hash值的后4位,

qq_36334929的博客 3076

[ vulhub漏洞复现篇 ] ActiveMQ反序列化漏洞复现(CVE-2015-5254)

Apache ActiveMQ是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持Java消息服务、集群、Spring Framework等。由于ActiveMQ是一个纯Java程序,因此只需要操作系统支持Java虚拟机,ActiveMQ便可执行。Apache ActiveMQ 5.13.0之前5.x版本中存在安全漏洞,该漏洞源于程序没有限制可在代理中序列化的类。......

qq_51577576的博客 4444

jdk1.8 HashMap底层数据结构:深入解析为什么jdk1.8 HashMap的容量一定要是2

深入解析jdk1.8 HashMap的容量一定要是2的n次幂的原因; jdk1.8 HashMap.resize()扩容方法源码解析。

GitChat 624

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

java中hashmap源码解析(jdk1.8)

java中hashmap常用方法流程解析,面试用

su_xu_chao的博客 742

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

深入理解HashMap的底层原理(二)

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 H...

陈橙橙 2780

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

HashMap底层原理解析JDK1.7与JDK1.8(二)

JDK1.8 通过put中的key去取到下标。key---->key.hashcode()----->然后去取余table.lenght------>inde-----0-7 hashCoude转换取余以后会转换成数组的下标比如1,2等。 用数组去存储的数据,指定下标去查找,时间复杂度O(1),对于一般的插入操作,涉及到数组元素的移动,平均复杂度是O(N). 数组长度16为什么要去减去1,然后进行与运算。 ** 重点 链表转化为红黑树,需要链表的长度大于8,但是在转变成R-B tree之

qq_41972358的博客 253

HashMap(jdk1.8)源码分析(二)

HashMap(jdk1.8)源码分析(二)HashMap如何计算索引计算公式为什么容量一定要是2的幂hash()有什么用如何保证容量一定为2的幂HashMap(int initialCapacity)一堆位运算的tableSizeFor方法我该如何看懂再谈resize()小结 HashMap如何计算索引 计算公式 int index = hash(key) & (length - 1)...

死灵镜面的博客 224

HashMap死循环及JDK1.8的resize()如何维护链表顺序

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34126557的博客 202

hashmap原理_深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)

介绍HashMap原理是JAVA和Android面试中经常会遇到的问题,这篇文章将通过HashMap在JDK1.7和1.8 中的源码来解析HashMap的原理。相关概念数组采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式...

weixin_39694838的博客 137

【Java集合】HashMap的resize()源码详解以及JDK1.7与JDK1.8的区别

HashMap的扩容源码详解,以及JDK1.7和JDK1.8的resize()方法的源码比较。

小七的博客 2754
上一篇: 计网-TCP-拥塞控制
下一篇: 算法-Group Anagrams-字母异位词分组
酱油瓶啤酒杯
博客等级 码龄8年 35粉丝 135原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值