常见面试题-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 实际的显存使用量取决于图像大小&#…

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

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

Nginx 修改server_name后无法访问

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

目标检测—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来让订单的这个微…

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

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

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

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

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

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

数据结构-哈希表(C语言)

哈希表的概念 哈希表就是: “将记录的存储位置与它的关键字之间建立一个对应关系,使每个关键字和一个唯一的存储位置对 应。” 哈希表又称:“散列法”、“杂凑法”、“关键字:地址法”。 哈希表思想 基本思想是在关键字和存…

电子器件系列44:环形线圈电感

干货!电感最重要、最常见的几个作用_线圈 环形线圈电感的原理: 电感中包含了哪三个物理学定律,为什么它能以磁场形式储能_哔哩哔哩_bilibili 电感的基本原理_哔哩哔哩_bilibili 环形线圈电感的作用: 1.储能器: 环形线圈电感能够…

学习c#的第十四天

目录 C# 接口(Interface) 接口的特点 定义接口 接口继承 接口和抽象类的区别 C# 命名空间(Namespace) using 关键字 定义命名空间 嵌套命名空间 C# 接口(Interface) 接口定义了所有类继承接口时应…

01ctfer 文件上传

01ctfer 文件上传 启动靶场 访问该地址 代码审计 <?php header("Content-Type:text/html; charsetutf-8"); // 每5分钟会清除一次目录下上传的文件 require_once(pclzip.lib.php);if(!$_FILES){echo <!DOCTYPE html> <html lang"zh">…

设计模式-备忘录模式-笔记

动机&#xff08;Motivation&#xff09; 在软件构建过程中&#xff0c;某些对象的状态在转换过程中&#xff0c;可能由于某种需要&#xff0c;要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些公有接口来让其他对象得到对象的状态&#xff0c;便会暴露对象的细节…