常见面试题-HashMap源码

news2025/1/10 20:57:48

了解 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.coloradmin.cn/o/1220986.html

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

相关文章

C语言--给定一行字符串,获取其中最长单词【图文详解】

一.问题描述 给定一行字符串,获取其中最长单词。 比如:给定一行字符串: hello wo shi xiao xiao su 输出:hello 二.题目分析 “打擂台算法”,具体内容小伙伴们可以参考前面的内容。 三.代码实现 char* MaxWord(const char* str)…

初始MySQL(七)(MySQL表类型和存储引擎,MySQL视图,MySQL用户管理)

目录 MySQL表类型和存储引擎 MyISAM MEMORY MySQL视图 我们先说说视图的是啥? 视图的一些使用细节 MySQL用户管理 原因 常见操作 MySQL表类型和存储引擎 -- 查看所有的存储引擎 SHOW ENGINES 我们常见的表有MyISAM InnoDB MEMORY 1.MyISAM不支持事务,也不支持外…

群晖7.2版本安装CloudDriver2(套件)挂载alist(xiaoya)到本地

CloudDrive是一个强大的多云盘管理工具,为用户提供包含云盘本地挂载的一站式的多云盘解决方案。挂载到本地后,可以像本地文件一样进行操作。 一、套件库添加矿神源 二、安装CloudDriver2 1、搜索安装 搜索框输入【clouddrive】,搜索到Clou…

抖音快手判断性别、年龄自动关注脚本,按键精灵开源代码!

这个是支持抖音和快手两个平台的,可以进入对方主页然后判断对方年龄和性别,符合条件的关注,不符合条件的跳过下一个ID,所以比较精准,当然你可以二次开发加入更多的平台,小红书之类的,仅供学习&a…

YOLO目标检测——PCB缺陷数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用:电子制造过程的质量控制、生产线的自动化检测、以及产品可靠性验证等方面数据集说明:PCB缺陷检测数据集,真实场景的高质量图片数据,数据场景丰富标签说明:使用lableimg标注软件标注,标注框质量…

修完这个 Bug 后,MySQL 性能提升了 300%

最近 MySQL 官方在 8.0.35 上修复了一个 bug: 这个 bug 是由 Mark Callaghan 发现的。Mark 早年在 Google MySQL 团队,后来去了 Meta MySQL,也主导了 RocksDB 的开发。 Mark 在 #109595 的 bug report 给出了非常详细的复现步骤 在官方修复后…

反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。 单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 注意:输入字符串 s中可能会存在前导空格、尾随空格…

一键转换bmp到jpg,轻松优化你的图片管理

在数字世界中,图片是我们日常工作和生活中的重要组成部分。然而,不同的文件格式可能会给我们的图片管理带来诸多不便。这时,我们就需要一个简单而强大的工具来帮助我们轻松转换图片格式。今天,我们为大家带来了一款全新的bmp到jpg…

Paypal发布公开信,三月已过,PYUSD发展如何?

2023年8月8日,美国支付巨头Paypal正式宣布推出其稳定币PYUSD,成为美国传统大型金融机构发行稳定币的首家企业。 当时,正值美国SEC与加密交易所Coinbase、Binance的诉讼白热化,Paxos被迫停止发行BUSD的阶段,在此背景下&…

【计算机网络学习之路】网络基础1

文章目录 前言一. 计算机网络发展局域网和广域网 二. 网络协议三. OSI七层模型四. TCP/IP四层(五层)模型五. 计算机体系结构与网络协议栈六. 协议形式及局域网通信数据包封装与分用 七. 跨网络通信八. MAC地址与网络通信的理解结束语 前言 本系列文章是…

IP池大小重要吗?

我们在寻找靠谱的IP代理时也经常遇到一个问题,IP代理池是什么?大小有何影响。今天就来跟大家普及一下,IP代理池大小的是否重要? 一、IP代理池是什么? I\P代理池是一个存储大量代理服务器IP地址的集合。它是一个由多个…

CTF-PWN环境搭建手册

工欲善其事必先利其器,作为一名CTF的pwn手,一定要有自己的专用解题环境。本文将详细记录kali下的pwn解题环境的安装过程,B站也会配备配套视频。 安装前的准备工作 虚拟机环境 VMware WorkStation VM版本安装教程 1. 下载Kali的VM虚拟机文件…

flink的window和windowAll的区别

背景 在flink的窗口函数运用中,window和windowAll方法总是会引起混淆,特别是结合上GlobalWindow的组合时,更是如此,本文就来梳理下他们的区别和常见用法 window和windowAll的区别 window是KeyStream数据流的方法,其…

[Linux版本Debian系统]安装cuda 和对应的cudnn以cuda 12.0为例

写在前面 先检查自己有没有安装使用wget的命令,没有的话输入下面命令安装: apt-get install wget -y查看gcc的安装 sudo apt install gcc #安装gcc gcc --version #查看gcc是否安装成功 #若上述命令不成功使用下面的命令尝试之后再执行上面…

VS Code中常用插件推荐

❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…

epoll协程简述

协程的由来 【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili 协程类别:有栈(静态)协程, 无栈(动态协程) 协程epoll 当有需要等待的时候,就切换出去,要用汇编保存这个栈rsp 运行时,要根据协程上下文恢复出这个栈

ArmV8常用汇编指令

1.syntax用法 GNU汇编器的.syntax .syntax命令是ARM架构独有的命令,语法为 .syntax [unified | divided];作用是在汇编ARM指令时,指定按照什么样的语法规则进行汇编。如果在编写汇编语言时不使用该命令指定语法规则,那么默认采用.…

TDengine Restful Authorization 自定义Token

Restful 接口是 TDengine 最常用的接口,仅次于 JDBC。TDengine 支持 HTTP 和 HTTPS,但通常情况下,大家不想搞证书,又在内网环境中,采用 HTTP 方式比较多。但 HTTP 是明文传输,只要抓个包就知道账号密码了。…

23111705[含文档+PPT+源码等]计算机毕业设计SSM框架美妆商城全套电商购物

文章目录 **软件开发环境及开发工具:****项目功能介绍:****论文截图:****实现:****代码片段:** 编程技术交流、源码分享、模板分享、网课教程 🐧裙:776871563 软件开发环境及开发工具&#xff…

uart控制led与beep

仲裁模块代码: // 外设控制模块,根据uart接收到的数据,控制led与beep的标志信号。 module arbit(input wire sys_clk ,input wire sys_rst_n ,input wire pi_flag …