常见面试题-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.rhkb.cn/news/195463.html

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

相关文章

Genio 500_MT8385安卓核心板:功能强大且高效

Genio 500(MT8385)安卓核心板是一款功能强大且高效的AIoT平台,内置的AI处理器(APU)工作频率可达500MHz,支持深度学习、神经网络加速和计算机视觉应用。配合高达2500万像素的摄像头,可以为AI相机应用提供清晰、精确的图像,如人脸识…

【Redis】springboot整合redis(模拟短信注册)

要保证redis的服务器处于打开状态 上一篇: 基于session的模拟短信注册 https://blog.csdn.net/m0_67930426/article/details/134420531 整个流程是,前端点击获取验证码这个按钮,后端拿到这个请求,通过RandomUtil 工具类的方法生…

【C#】类型转换-显式转换:括号强转、Parse法、Convert法、其他类型转string

目录 一、括号强转 1.有符号整型 2.无符号整型 3.浮点之间 4.无符号和有符号 5.浮点和整型 6.char和数值类型 7.bool和string是不能够通过 括号强转的 二、Parse法 1.有符号 2.无符号 3.浮点型 4.特殊类型 三、Convert法 1.转字符串 2.转浮点型 3.特殊类型转换…

AdaBoost:提升机器学习的力量

一、介绍 机器学习已成为现代技术的基石,为从推荐系统到自动驾驶汽车的一切提供动力。在众多机器学习算法中,AdaBoost(Adaptive Boosting的缩写)作为一种强大的集成方法脱颖而出,为该领域的成功做出了重大贡献。AdaBoo…

【2016年数据结构真题】

已知由n&#xff08;M>2&#xff09;个正整数构成的集合A{a<k<n},将其划分为两个不相交的子集A1 和A2&#xff0c;元素个数分别是n1和n2&#xff0c;A1和A2中的元素之和分别为S1和S2。设计一个尽可能高效的划分算法&#xff0c;满足|n1-n2|最小且|s1-s2|最大。要求…

编译智能合约以及前端交互工具库(Web3项目一实战之三)

我们已然在上一篇 Web3项目灵魂所在之智能合约编写(Web3项目一实战之二) ,为项目写好了智能合约代码。 但身为开发人员的我们,深知高级编程语言所编写出来的代码,都是需要经过编译,而后外部方能正常调用。很显然,使用solidity这门新的高级编程语言编写出来的智能合约,也…

【数据结构】栈与队列的实现

栈与队列是数据结构中重要的结构&#xff0c; 可以用于解决一些题目 模拟实现时可以增加对于这些结构的理解&#xff0c;也可以巩固我们的语言水平&#xff0c;解决某些题目也会有很好的效果 话不多说 目录 栈的实现结构体的定义&#xff1a;初始化栈:压栈&#xff1a;出栈&am…

【MySQL】表的增删改查(进阶)

一、数据库约束 1.1 约束类型 &#x1f693;NOT NULL - 指示某列不能存储 NULL 值。 &#x1f693;UNIQUE - 保证某列的每行必须有唯一的值。 &#x1f693;DEFAULT - 规定没有给列赋值时的默认值。 &#x1f693;PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列&…

基于探路者算法优化概率神经网络PNN的分类预测 - 附代码

基于探路者算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于探路者算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于探路者优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…

Redis持久化策略之RDB与AOF

文章目录 1.RDB1)基本介绍2)自动触发3)手动触发4)RDB文件5)优点缺点 2.AOF1)基本介绍2)使用方式3)工作流程4)重写机制5)AOF文件6)优点缺点 3.RDB AOF 我们都知道&#xff0c;redis 是一个基于内存的数据库。基于内存的好处是访问速度快&#xff0c;缺点是“不持久”——当数据…

Git常用规范

分支命名规范 Git分支命名规范可以根据具体的项目和团队的需要而有所不同&#xff0c;但是以下是一些常见的规范&#xff1a; 主分支&#xff08;master/main&#xff09;&#xff1a;这个分支通常是主要的稳定分支&#xff0c;它包含了当前生产环境的代码。在一些项目中&…

CMakeLists.txt基础指令与cmake-gui生成VS项目的步骤

简介 本博客主要介绍cmake的基本指令&#xff0c;同时&#xff0c;很多使用Visual Studio小白从Gitbub下载项目源码后&#xff0c;看到CMakeLists.txt&#xff0c;不知道如何使用Visual Studio编译源码&#xff1b;针对以上问题&#xff0c;做一下简单操作与解释&#xff0c;方…

Ingress安全网关

目录 文章目录 目录本节实战TCP 流量拆分&#x1f6a9; 实战&#xff1a;TCP 流量拆分-2023.11.15(测试成功) Ingress安全网关Kubernetes Ingress&#x1f6a9; 实战&#xff1a;Kubernetes Ingress-2023.11.15(测试成功) Ingress GatewayIngress Gateway&#x1f6a9; 实战&am…

m1 rvm install 3.0.0 Error running ‘__rvm_make -j8‘

在使用M1 在安装cocopods 前时&#xff0c;安装 rvm install 3.0.0遇到 rvm install 3.0.0 Error running __rvm_make -j8 备注: 该图片是借用其他博客图片&#xff0c;因为我的环境解决完没有保留之前错误信息。 解决方法如下&#xff1a; 1. brew uninstall --ignore-depe…

Java NIO 详解

一、NIO简介 NIO 是 Java SE 1.4 引入的一组新的 I/O 相关的 API&#xff0c;它提供了非阻塞式 I/O、选择器、通道、缓冲区等新的概念和机制。相比与传统的 I/O 多出的 N 不是单纯的 New&#xff0c;更多的是代表了 Non-blocking 非阻塞&#xff0c;NIO具有更高的并发性、可扩…

es head 新增字段、修改字段、批量修改字段、删除字段、删除数据、批量删除数据

目录 一、新增字段 二、修改字段值 三、批量修改字段值 ​四、删除字段 五、删除数据/文档 六、批量删除数据/文档 一、新增字段 put http://{ip}:{port}/{index}/_mapping/{type} 其中&#xff0c;index是es索引、type是类型 数据&#xff1a; {"_doc"…

数据结构与算法之美学习笔记:20 | 散列表(下):为什么散列表和链表经常会一起使用?

目录 前言LRU 缓存淘汰算法Redis 有序集合Java LinkedHashMap解答开篇 & 内容小结 前言 本节课程思维导图&#xff1a; 今天&#xff0c;我们就来看看&#xff0c;在这几个问题中&#xff0c;散列表和链表都是如何组合起来使用的&#xff0c;以及为什么散列表和链表会经常…

window 搭建 MQTT 服务器并使用

1. 下载 安装 mosquitto 下载地址&#xff1a; http://mosquitto.org/files/binary/ win 使用 win32 看自己电脑下载相应版本&#xff1a; 一直安装&#xff1a; 记住安装路径&#xff1a;C:\Program Files\mosquitto 修改配置文件&#xff1a; allow_anonymous false 设置…

【VSCode】Visual Studio Code 下载与安装教程

前言 Visual Studio Code&#xff08;简称 VS Code&#xff09;是一个轻量级的代码编辑器&#xff0c;适用于多种编程语言和开发环境。本文将介绍如何下载和安装 Visual Studio Code。 下载安装包 首先&#xff0c;我们需要从官方网站下载 Visual Studio Code 的安装包。请访…

Docker与VM虚拟机的区别以及Docker的特点

01、本质上的区别 VM(VMware)在宿主机器、宿主机器操作系统的基础上创建虚拟层、虚拟化的操作系统、虚拟化的仓库&#xff0c;然后再安装应用&#xff1b; Container(Docker容器)&#xff0c;在宿主机器、宿主机器操作系统上创建Docker引擎&#xff0c;在引擎的基础上再安装应…