常见面试题-HashMap源码

了解 HashMap 源码吗?

参考文章:https://juejin.cn/post/6844903682664824845

https://blog.51cto.com/u_15344989/3655921

以下均为 jdk1.8 的 HashMap 讲解

首先,HashMap 的底层结构了解吗?

底层结构为:数组 + 链表 + 红黑树

什么时候链表会转换为红黑树呢?

当一个位置上哈希冲突过多时,会导致数组中该位置上的链表太长,链表的查询时间复杂度是O(N),即查询代价随着链表长度线性增长,那么在 HashMap 中就通过 TREEIFY_THRESHOLD=8 来控制链表的长度,当链表的长度大于 8 时并且数组长度大于 64 时,就将链表转换为红黑树

这里在冲突插入链表时,使用的是尾插法,会顺着链表进行判断,当遍历到链表最后一个节点时,并判断链表长度是否需要转为红黑树,之后再通过尾插法,插入在最后一个节点的后边

扩展:jdk8 之前是头插法,但是 jdk8 改为了尾插法,这是为什么呢?为什么 jdk8 之前要采用头插法呢?

jdk1.7 使用头插法的一种说法是,利用到了缓存的时间局部性,即最近访问过的数据,下次大概率还会进行访问,因此把刚刚访问的数据放在链表头,可以减少查询链表的次数

jdk1.7 中的头插法是存在问题的,在并发的情况下,插入元素导致扩容,在扩容时,会改变链表中元素原本的顺序,因此会导致链表成环的问题

那么 jdk8 之后改为了尾插法,保留了元素的插入顺序,在并发情况下就不会导致链表成环了,但是 HashMap 本来就不是线程安全的,如果需要保证线程安全,使用 ConcurrentHashMap 就好了!

如何计算插入节点在数组中需要存储的下标呢?

计算下标是先计算出 key 的 hash 值,在将 hash 值对数组长度进行取模,拿到在数组中存放的位置

计算 hash 值代码如下:

(h = key.hashCode()) ^ (h >>> 16)

首先拿到 key 的 hashCode,将 hashCode 和 h >>> 16 进行异或运算,此时计算出来 key 的哈希值 hash,这里计算 哈希值 时,因为在计算数组中的下标时,会让 hash 值对数组长度取模,一般数组长度不会太大,导致 hash 值的高 16 位参与不到运算,因此让 hashCode 在与 hashCode >>> 16 进行异或操作,让 hashCode 的高 16 位也可以参与到下标的计算中去,这样计算出的下标更不容易冲突

这里面试官问了 hashCode 一定是 32 位吗?当时没反应过来,其实一定是 32 位的,因为 hashCode 是 int 类型,这里说的 32 位其实是二进制中是 32 位,int 类型是 4B = 32bit

那么在数组中的下标为:hash & (n-1) 也就是让 hash 值对数组长度进行取模,从而拿到在数组中的下标。(这里 hash & (n-1) == hash % n,hash 值和 n-1 进行与操作其实就是使用二进制运算进行取模)

这里举个取模运算的例子:

比如数组长度为 8,计算出来的 hash 值为 19,那么

19 & (8 - 1) = 10011 & 00111(二进制) = 00011(二进制) = 3

19 % 8 = 3

HashMap 中如何进行扩容的呢?

当 HashMap 中的元素个数超过数组长度 * loadFactor(负载因子)时,就会进行数组扩容,负载因子默认为 0.75,数组大小默认为 16,因此默认是 HashMap 中的元素个数超过 (16 * 0.75 = 12) 时,就会将数组的大小扩展为原来的一倍,即 32,之后再重新计算数组的下标,这异步操作是比较耗费性能的,所以如果可以预知 HashMap 中元素的个数,可以提前设置容量,避免频繁的扩容

在 HashMap 扩容时,即在 resize() 方法中,如果数组中某个位置上的链表有多个元素,那么我们如果对整条链表上的元素都重新计算下标是非常耗时的操作,因此在 HashMap 中进行了优化,HashMap 每次扩容都是原来容量的 2 倍,那么一条链表上的数据在扩容之后,这一条链表上的数据要么在原来位置上,要么在原来位置+原来数组长度上,这样就不需要再对这一条链表上的元素重新计算下标了,下边来解释一下为什么这一条链表扩容后的位置只可能是这两种情况:

因为每一次扩容都是容量翻倍,在下标计算中 (n-1) & hash 值,n 每次扩容都会增大一倍,那么 (n-1) 在高位就会多一个 1,比如(可能写的有些啰嗦,主要是这一段用文字不太好描述,耐心看一下就可以看懂):

假如说我们插入一个 key="zqy" 时,从 16 扩容为 32 ,我们来看一下扩容前后的如何计算下标:

  • n 为 16 时,n-1 只有 4 个 1
  • n 为 32 时,n-1 有 5 个 1,在高位多出来了一个 1

在这里插入图片描述

下标的计算公式为 (n-1)&hash,n 每次都是扩容1倍,也就是 n-1 的二进制中会在高位多一个 1,那么如果 hash 值在多出来的 1 这一位上为 1,那么下标计算之后就比原下标多了一个 oldCap,如果 hash 值在多出来的 1 这一位上为 0,那么就不会对下标计算有影响,新下标还是等于原下标

那么怎么判断在多出来的这一个 1 的位置上,hash 值是否为 1 呢?只需要让 hash & oldCap 即可,对上图来说,在扩容之后,当 n 为 32 时, n-1 中会多出来标位红色的1,那么需要判断的就是"zqy"的 hash 值中绿色的位置那一位是否为1(通过 hash&oldCap 来判断),如果为1,新下标=原下标+oldCap;如果为 0,新下标=原下标

上边说的源码位置如下图,下边为 resize() 方法中的部分代码,优化位置在 738742 行,在 715 行开始的 else 语句中,针对的就是原数组的位置上的链表有多个元素,在 721 行判断,如果 hash & oldCap 是 0 的话,表示该链表上的元素的新下标为原下标;如果是 1,表示新下标=原下标+原数组长度

在这里插入图片描述

HashMap 在链表长度达到 8 之后一定会转为红黑树吗?如何转为红黑树呢?

HashMap 会在数组长度大于 64 并且链表长度大于 8 才会将链表转为红黑树

在下边这个转成红黑树的方法中,757 行就判断了 tab.length 也就是数组的长度,如果小于 64,就进行扩容,不会将链表转成红黑树

如果需要转换成红黑树,就进入到 759 行的 if 判断,先将链表的第一个节点赋值为 e,之后将 e 转为 TreeNode,并且将转换后的树节点给串成一个新的链表,hd 为链表头,tl 为链表尾,当将链表所有节点转为 TreeNode 之后,在 771 行使用转换后的双向链表替代原来位置上的单链表,之后再 772 行调用 treeify() ,该方法就是将链表中的元素一个一个插入到树中

在这里插入图片描述

HashMap不是线程安全的,那么举一个不安全的例子吧?

我们可以来分析一下,在多线程情况下,那么一般是多个线程修改同一个 HashMap 所导致的线程不安全,那么也就是 put() 操作中,会造成线程不安全了,那么我们看下边 putVal() 方法,来分析一下在哪里会造成线程不安全:

假如初始时,HashMap 为空,此时线程 A 进到 630 行的 if 判断,为 true,当线程 A 准备执行 631 行时,此时线程 B 进入在 630 行 if 判断发现也为 true,于是也进来了,在 631 行插入了节点,此时线程 B 执行完毕,线程 A 继续执行 631 行,就会出现线程 A 插入节点将线程 B 插入的节点覆盖的情况

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/145952.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis常见问题及解决方案

缓存预热 定义 缓存预热是一种优化方案,它可以提高用户的使用体验。 缓存预热是指在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,节省用户等待时间 实现思路 把需要缓存的方法写在初始化方…

【MySQL】聚合函数:汇总、分组数据

文章目录 学习目标MAX()、MIN()、AVG()、SUM()、COUNT()COUNT(*) 得到所有记录条目DISTINCT去重练习1(使用UNION , SUM, BETEEN AND)GROUP BY子句练习2(使用sum,group by, join on, …

S25FL256S介绍及FPGA实现思路

本文介绍 S25FL256S 这款 FLASH 芯片,并进行 FPGA 读写控制的实现(编程思路及注意事项)。 文章目录 S25FL-S 介绍管脚功能说明SPI 时钟模式SDRDDR 工作模式FLASH存储阵列(地址空间映射)常用寄存器及相关指令Status Reg…

Stable Diffusion WebUI使用AnimateDiff插件生成动画

AnimateDiff 可以针对各个模型生成的图片,一键生成对应的动图。 配置要求 GPU显存建议12G以上,在xformers或者sdp优化下显存要求至少6G以上。 要开启sdp优化,在启动参数加上--sdp-no-mem-attention 实际的显存使用量取决于图像大小&#…

毫米波雷达模块的目标检测与跟踪

毫米波雷达技术在目标检测与跟踪方面具有独特的优势,其高精度、不受光照影响等特点使其在汽车、军事、工业等领域广泛应用。本文深入探讨毫米波雷达模块在目标检测与跟踪方面的研究现状、关键技术以及未来发展方向。 随着科技的不断进步,毫米波雷达技术在…

硬件测试与EMC测试到底测些啥?

今天说一下个人的经验之谈,主要偏硬件测试一些的东西。 嵌入式主要分为软件和硬件,但是想要做好一个产品只是做出来还不行,还需要最后的验证,即测试了。 嵌入式行业我接触到的主要就是嵌入式软件工程师、嵌入式硬件工程师…

Nginx 修改server_name后无法访问

问题: 在nginx.conf配置中, server_name 为 localhost 时可以正常访问,但改成自定义的域名后无法访问 解决方法: - Window系统 修改本地hosts文件,一般路径在:C:\Windows\System32\drivers\etc\hosts 在文件最后…

快速弄懂C++中的this指针

作用: 防止在定义类的时候出现同名变量(如:定义了一个私有变量,在共有域中用一个方法对私有变量进行了赋值,且赋值的变量与私有域变量同名,此时只有使用this指针才能进行赋值)能够在定义的类内…

【解决】使用Element-Plus icon图标不显示

使用Element-Plus icon图标不显示的解决方案 博主环境:Vue3 TypeScript 已经安装:element-plus/icons-vue 就是不显示图标,但也不报错 我的解决方法: 根据官网指引,在main.ts(如果是JavaScript就是main.…

目标检测—YOLO系列(二 ) 全面解读论文与复现代码YOLOv1 PyTorch

精读论文 前言 从这篇开始,我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法,速度快且结构简单,其他的目标检测算法如RCNN系列,以后有时间的话再介绍。 本文主要介绍的是YOLOV1,这是由以Joseph Redmon为首的…

Nginx配置开启HTTPS

获取证书文件 Nginx 开启SSL server {listen 443 default ssl;server_name localhost;#charset koi8-r;#access_log logs/host.access.log main;proxy_set_header Host $host;ssl_certificate /usr/local/nginx/cert/server.pem;ssl_certificate_key /usr/local/ngin…

京东推出数据平台云海 API接口将达700个

1月16日消息,继上周面对企业用户发布京东电商云解决方案后,日前,京东云平台又发布了全新的数据开放平台——“云海”,以开放商家、商品、点击流等相关数据。 在京东主办,思路网协办的京东开放云服务合作伙伴2014峰会&…

[CSS] 文本折行

文本折行一般分为两种情况: CJK(Chinese/Japanese/Korean) 字符和非 CJK 字符。一般非 CJK 字符折行发生在两个单词的空格中间,见下图: 图中文本 “hello world” 包裹容器的宽度为 2rem,但是 hello 并没有…

JavaWeb[总结]

文章目录 一、Tomcat1. BS 与 CS 开发介绍1.1 BS 开发1.2 CS 开发 2. 浏览器访问 web 服务过程详解(面试题)2.1 回到前面的 JavaWeb 开发技术栈图2.2 浏览器访问 web 服务器文件的 UML时序图(过程) ! 二、动态 WEB 开发核心-Servlet1. 为什么会出现 Servlet2. 什么是…

微服务学习 | Eureka注册中心

微服务远程调用 在order-service的OrderApplication中注册RestTemplate 在查询订单信息时,需要同时返回订单用户的信息,但是由于微服务的关系,用户信息需要在用户的微服务中去查询,故需要用到上面的RestTemplate来让订单的这个微…

C++中sort()函数的greater<int>()参数

目录 1 基础知识2 模板3 工程化 1 基础知识 sort()函数中的greater<int>()参数表示将容器内的元素降序排列。不填此参数&#xff0c;默认表示升序排列。 vector<int> a {1,2,3}; sort(a.begin(), a.end(), greater<int>()); //将a降序排列 sort(a.begin()…

Linux网络之传输层协议tcp/udp

文章目录 目录 一、再谈端口号 1.端口号划分 2.知名端口号 3.netstat&#xff0c;pidof 二、UDP协议 1.udp协议格式 2.udp特点 3.基于udp的应用层协议 三、TCP协议 1.tcp报头 确认应答机制&#xff08;ACK) 超时重传机制 连接管理机制&#xff08;三次握手四次挥…

前端案例-css实现ul中对li进行换行

场景描述&#xff1a; 我想要实现&#xff0c;在展示的item个数少于4个的时候&#xff0c;则排成一行&#xff0c;并且均分&#xff08;比如说有3个&#xff0c;则每个的宽度为33.3%&#xff09;&#xff0c;如果item 个数大于4&#xff0c;则进行换行。 效果如下&#xff1a…

Django_学习_01

Django 项目快速创建及目录说明 1.先创建虚拟环境 (是创建一个相对隔离的环境,后面安装的第三方包都在虚拟环境中--可以理解成一个容器) 先创建一个项目根目录,创建后进入到这个目录创建对应的虚拟目录,例如已经创建了project_django1 a.创建虚拟环境 b.激活虚拟环境 c.安装虚…

【VSCode】配置C/C++开发环境教程(Windows系统)

下载和配置MinGW编译器 首先&#xff0c;我们需要下载并配置MinGW编译器。 下载MinGW编译器&#xff0c;并将其放置在一个不含空格和中文字符的目录下。 配置环境变量PATH 打开控制面板。可以通过在Windows搜索栏中输入"控制面板"来找到它。 在控制面板中&#xf…