面试必问究极重点之HashMap的底层原理

1.底层数据结构

        JDK版本不同的数据结构

  • 1.7 数组 + 链表

  • 1.8 数组 + (链表 | 红黑树)

2.添加数据put

  1. 在添加一个值的时候,首先会计算他的hash码,然后进行二次hash,在对当前长度取模得到在底层数组中的索引位置
  2. 当取模完成后,会遇到不同元素索引位置相同的情况。我们把这种情况叫做hash冲突,此时会将后一个元素通过链表的形式挂在下边
  3. 当存储元素数量超过数组容量的四分之三时,会进行扩容,扩容后,也可以减少链表长度。
  4. 但是如果同一条链上的元素原始hash本就相同,此时通过扩容就不能有减少链表的长度了

3.树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略

  • hash 表的查找,更新的时间复杂度是 $O(1)$,而红黑树的查找,更新的时间复杂度是 $O(log_2⁡n )$,TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表

  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表

  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表(在移除之前检查

4.索引计算

索引计算方法

  • 首先,计算对象的 hashCode()

  • 再进行调用 HashMap 的 hash() 方法进行二次哈希

    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀

  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模

  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash

  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

5.put与扩容

put 流程

  1. HashMap 是懒惰创建数组的,首次使用才创建数组

  2. 计算索引(桶下标)

  3. 如果桶下标还没人占用,创建 Node 占位返回

  4. 如果桶下标已经有人占用

    1. 已经是 TreeNode 走红黑树的添加或更新逻辑

    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

  5. 返回前检查容量是否超过阈值,一旦超过进行扩容

1.7 与 1.8 的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f

        当扩容的个数 > 数组长度*负载因子的值

  1. 在空间占用与查询时间之间取得较好的权衡

  2. 大于这个值,空间节省了,但链表就会比较长影响性能

  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

6.多线程下HashMap会有什么问题

  • 扩容死链(1.7
    • 出现这个问题的主要原因是,在多线程情况下,扩容时,需要把元素从新放入新数组中,那么在同一位置上的元素会顺序放入新数组中,1.7采用的是头插法从而导致了扩容死链问题。
  • 数据错乱(1.7 1.8

        多个线程同时操作HashMap会出现,数据丢失的情况,是因为在添加元素时,可能在同一位置需要添加多个元素,但是会出现覆盖情况。

7.key 的设计

        key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然

  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)如果key是可变的,那么你在HashMap去查询时,它的HashCode就不一样了,也就找不到数据了。

  3. key 的 hashCode 应该有良好的散列性

        

        String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特

  • 字符串中的每个字符都可以表现为一个数字,称为 S_i,其中 i 的范围是 0 ~ n - 1

  • 散列公式为: S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0

  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为

    • 即 32 ∗h -h 

    • 即 2^5 ∗h -h

    • 即 h≪5 -h

8.源码分析

8.1常量

    //默认初始容量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大容纳数//最大容量,如果使用参数的任一构造函数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30。static final int MAXIMUM_CAPACITY = 1 << 30;//负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//使用树而不是列表的箱计数阈值。将元素添加到至少具有此多个节点的图格时,图格将转换为树。该值必须大于 2,并且应至少为 8,以与树木移除中关于在收缩时转换回普通条柱的假设相吻合。//阈值 作用于 树化static final int TREEIFY_THRESHOLD = 8;//取消树化阈值static final int UNTREEIFY_THRESHOLD = 6;//最小树化容量  static final int MIN_TREEIFY_CAPACITY = 64;

在成员变量中可以发现

        HashMap定义了默认的初始容量负载因子树化阈值退出树化阈值最小树化数组的容量以及最大容量

8.2成员变量

//该表在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。(我们还允许在某些操作中使用长度为零,以允许当前不需要的引导机制。transient Node<K,V>[] table;//保存缓存的 entrySet()。请注意,AbstractMap 字段用于 keySet() 和 values()。transient Set<Map.Entry<K,V>> entrySet;//此映射中包含的键值映射数。transient int size;//此 HashMap 在结构上被修改的次数 结构修改是指更改 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的修改。此字段用于使 HashMap 的 Collection-views 上的迭代器快速失败。(请参阅 ConcurrentModificationException)。transient int modCount;//要调整大小的下一个大小值(容量 * 负载系数)。int threshold;//哈希表的负载因子final float loadFactor;

8.3构造函数

/** 
构造一个具有指定初始容量和负载因子的空 HashMap。
参数: 
initialCapacity – 初始容量 
loadFactor – 负载系数抛出: IllegalArgumentException – 如果初始容量为负或负载系数为非正
*/public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}
/**
构造一个空的 HashMap,具有指定的初始容量和默认负载系数 (0.75)。
参数: initialCapacity – 初始容量。
抛出:IllegalArgumentException – 如果初始容量为负数。
*/
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}
//使用默认初始容量 (16) 和默认负载系数 (0.75) 构造一个空的 HashMap。
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认}
/**
使用与指定 Map 相同的映射构造新的 HashMap。
HashMap 是使用默认负载系数 (0.75) 创建的,初始容量足以在指定的 Map 中保存映射。
参数: m – 其映射将放置在此映射中的映射 
Throws: NullPointerException – 如果指定的映射为 null
*///参数时一个Map集合的话,就直接添加进去
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

8.4get

/*
返回指定键映射到的值,如果此映射不包含键的映射,则返回 null。
更正式地说,如果此映射包含从键 k 到值 v 的映射,
使得 (key==null ? k==null : key.equals(k)),则此方法返回 v;
否则,它将返回 null。(最多可以有一个这样的映射。
返回值 null 并不一定表示映射不包含键的映射;
映射也有可能将键显式映射到 null。containsKey 操作可用于区分这两种情况。
参见:put(Object, Object)
**/public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}

8.5put

//参数为 key,value 的形式 
public V put(K key, V value) {//调用了putVal方法return putVal(hash(key), key, value, false, true);}/*
参数: hash – 键键的哈希值 – 键值 – 要放置的值onlyIfAbsent – 如果为 true,则不更改现有值 evict – 如果为 false,则表处于创建模式。这个函数实现了Java中的Map的put方法及其相关方法。
它根据给定的键和值,将键值对添加到Map中。
如果键已存在且onlyIfAbsent为true,则不更改现有的值。
如果evict为false,则表处于创建模式。如果evict为true且表已满,则会清理最久未访问的键值对。
最后,该函数会调整Map的大小并返回旧的值。
**/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

8.6Node

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

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

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

相关文章

electron——查看electron的版本(代码片段)

electron——查看electron的版本(代码片段)1.使用命令行&#xff1a; npm ls electron 操作如下&#xff1a; 2.在软件内使用代码&#xff0c;如下&#xff1a; console.log(process) console.log(process.versions.electron) process 里包含很多信息&#xff1a; process详…

【Linux】——基本指令(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——数据结构学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 导读&#xff1a;1. vim 指令2. head指令3. tail指令4. tree指令5. 输出重定向6. echo指令7. wc指令8. | 字符9. date指令…

PCIe 6.0生态业内进展分析总结

上一篇&#xff0c;我们针对PCIe 6.0的功能更新与实现挑战做了简单的分析与总结。更多详细内容可以参考&#xff1a; 扩展阅读&#xff1a;浅析PCIe 6.0功能更新与实现的挑战 那么&#xff0c;PCIe 6.0已经发布了一段时间了&#xff0c;业内硬件支持PCIe 6.0目前有哪些进展呢…

面试算法93:最长斐波那契数列

题目 输入一个没有重复数字的单调递增的数组&#xff0c;数组中至少有3个数字&#xff0c;请问数组中最长的斐波那契数列的长度是多少&#xff1f;例如&#xff0c;如果输入的数组是[1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6&#xff0c;7&#x…

外包干了5个月,技术明显退步了...

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年12月份&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…

周鸿祎分享大模型十大趋势:2024将出现杀手级应用

1月5日&#xff0c;“2023年风马牛年终秀”上&#xff0c;三六零&#xff08;601360.SH&#xff0c;下称“360”&#xff09;集团创始人周鸿祎分享了对2024年大模型发展趋势的十大预测&#xff0c;呼吁企业树立AI信仰&#xff0c;All in AI。他认为&#xff0c;创新才能破局&am…

ctfshow——信息搜集

文章目录 web 1web 2web 3web 4web 5web 6web 7web 8web 9web 10web 11web 12web 13web 14web 15web 16web 17web 18web 19web 20 web 1 题目提示开发注释未及时删除。 直接右键查看源代码。 web 2 在这关我们会发现&#xff1a;1&#xff09;无法使用右键查看源代码&…

Linux服务器的几种类型

Linux是一个开源操作系统内核&#xff0c;用作各种Linux发行版&#xff08;也称为“distros”&#xff09;的核心组件。由Linus Torvalds于1991年开发&#xff0c;Linux基于Unix操作系统。它以其稳定性、安全性和多功能性而闻名。 Linux的关键特点&#xff1a; 开源性质&#…

OpenCASCADE MFC例子

OpenCASCADE MFC例子 说明 一直对OpenCASCADE一直都比较感兴趣&#xff0c;这个例子是我参考这位大神C幼儿园中班小朋友的专栏做出来的OpenCASCADE_C幼儿园中班小朋友的博客-CSDN博客 不过我用的是vcpkg的方式安装OpenCASCADE&#xff0c;这个需要注意一下&#xff0c;可能需…

[蓝桥杯学习] 树链剖分

定义 将树分割成若干条链&#xff0c;以维护树上的信息&#xff0c;若无特殊需求&#xff0c;一般是重链剖分。 重链剖分 如何重链剖分 两个dfs 第一个dfs是预处理各个结点的基本信息&#xff0c;第二个dfs是利用信息进行剖分&#xff08;dfs序&#xff09; 操作步骤 第一…

git在本地创建dev分支并和远程的dev分支关联起来

文章目录 git在本地创建dev分支并和远程的dev分支关联起来1. 使用git命令2. 使用idea2.1 先删除上面建的本地分支dev2.2 通过idea建dev分支并和远程dev分支关联 3. 查看本地分支和远程分支的关系 git在本地创建dev分支并和远程的dev分支关联起来 1. 使用git命令 git checkout…

构建高效秒杀系统的设计原理及注意事项

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

设计模式——迭代器模式(Iterator Pattern)

概述 迭代器模式(Iterator Pattern)&#xff1a;提供一种方法来访问聚合对象&#xff0c;而不用暴露这个对象的内部表示&#xff0c;其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。 在软件开发中&#xff0c;我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有…

UG装配-沿线运动

如果希望图中圆柱销沿着槽运动&#xff0c;直接约束面是困难的&#xff0c;我们可以画出圆弧的中心线和圆柱销的中心点&#xff0c;约束点在线上&#xff0c;进行移动 需要注意的是&#xff0c;我们在零件中画点和线的时候&#xff0c;在装配体默认加载模型引用集的时候是无法显…

最新ChatGPT网站源码,支持Midjourney绘画,GPT语音对话+GPT-4识图理解能力+ChatFile文档对话总结+DALL-E3文生图

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作Ch…

基于 ESP32-C3 开启 Flash 加密和安全启动并进行 OTA 测试

软件&#xff1a; esp-idf v5.1.2 硬件&#xff1a; ESP32-C3 board 1. 首先&#xff0c;准备一个明文固件 hello-world.bin 基于 esp-idf-v5.1.2\examples\get-started\hello_world 例程&#xff0c;使用如下指令&#xff0c;直接编译&#xff0c;获取明文固件 hello-worl…

IDEA中自动导包及快捷键

导包设置及快捷键 设置&#xff1a;Setting->Editor->General->Auto import快捷键 设置&#xff1a;Setting->Editor->General->Auto import java区域有两个关键选项 Add unambiguous imports on the fly 快速添加明确的导包 IDEA将在我们书写代码的时候…

wpsjs学习——获取单元格批注

1.获取第一个单元格的值&#xff1a; wps.Application.ActiveSheet.Range(A1).Value2; 2.1.获取第一个单元格的批注&#xff1a; wps.Application.ActiveSheet.Range(A1).Comment.Text(); <div class"global">获取表格信息<div class"divItem">…

厚积薄发11年,鸿蒙究竟有多可怕

​12月20日中国工程院等权威单位发布《2023年全球十大工程成就》。本次发布的2023全球十大工程成就包括“鸿蒙操作系统”在内。入围的“全球十大工程成就”&#xff0c;主要指过去五年由世界各国工程科技工作者合作或单独完成且实践验证有效的&#xff0c;并且已经产生全球影响…

Zernike多项式法生成相位理论推导及图像引导实现原理

目录 引言 波前传感器 ​编辑 关于相位计算问题补充 关于结构图的修正 光束质量评价指标 Zernike多项式 ​编辑Zernike多项式法生成相位 光强分布求波前相位-GS 更快的迭代方法SPGD 基于Zernike模式的SPGD 引言 我们还是先从第一篇文献开始理解展开今天分享的一些重…