Java集合 - HashMap

news2024/11/14 20:18:24

介绍 HashMap

Map 是一种存储键值对的集合。Map 集合可以根据 key 快速查找对应的 value 值。HashMap 是 Map 类型的一中。

HashMap 的底层存储结构是:数组 + 链表 + 红黑树。

下面我们通过 HashMap 的新增操作、查找操作来看 HashMap 的底层存储结构。

1638547825959-e25260c0-b272-40f0-8fb8-036b2582db3d.png

HashMap 的新增操作

当调用 HashMap 的 put() 方法时,put() 方法的处理逻辑如下:

  • 首先,它会调用 hash() 方法根据 key 计算出 hash 值,然后根据计算出的 hash 值计算出 key 对应的数组索引 i:

  • 计算出 key 对应的数组索引 i 之后,它根据数组在索引 i 上的值进行处理:

    • 如果数组在索引 i 上的值为 null,则直接生成一个新的节点,并让 tab[i] 指向该节点;
    • 如果数组在索引 i 上的值不为 null,则意味着需要解决 hash 冲突问题。
  • 接上一步骤,如果数组在索引 i 上的值不为 null。

    • 如果索引 i 上的结构是普通链表,则把新生成的节点加到链表的末尾
    • 如果索引 i 上的结构是红黑树,则使用红黑树方式新增
  • 接上一步骤,如果索引 i 上的结构是普通链表,则把新生成的节点加到链表的末尾之后,需要判断是否需要将链表转为红黑树:

    • 如果链表的长度大于等于 8,并且数组的长度大于等于 64,则调用 treeifyBin() 将链表转为红黑树;
    • 如果链表的长度大于等于 8,但是数组的长度小于 64,则调用 resize() 方法执行扩容操作;
    • 当红黑树中的节点个数小于等于 6 时,红黑树会转为链表。
  • 将节点加入 HashMap 集合之后,put() 方法的最后一步,如果 HashMap 中元素的数量超过了扩容的阈值(threshold),那么它会调用 resize() 方法执行扩容操作。

当调用 HashMap 的 put() 方法时,如果 HashMap 中已经存在要新增的 key,并且方法的入参 onlyIfAbsent 为 false,则替换旧值,并返回旧值。


HashMap 中调用 hash() 方法根据 key 计算出 hash 值的规则是:

  • 如果 key 为 null,则计算出的 hash 值为 0
  • 如果 key 不为 null,则 hash 值的计算公式为 hash = key.hashCode() ^ (key.hashCode() >>> 16)。先将 key 的 hashCode 值无符号右移 16 位,然后再和 key 的 hashCode 值做 异或 运算,使 key 的 hashCode 值高 16 位的变化映射到低 16 位中,使 hashCode 值高 16 位也参与后续索引 i 的计算(i = hash & (n - 1))。减少了碰撞的可能性。
// 向 HashMap 集合中新增键值对
// 如果 HashMap 集合中已经存在该键,那么旧的值将被替换
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap 的扩容机制

当调用 HashMap 的 put() 方法时,将节点加入 HashMap 集合之后,如果 HashMap 中元素的数量超过了扩容的阈值(threshold),那么它会调用 resize() 方法执行扩容操作。

HashMap 的扩容机制是扩容为原来容量的 2 倍。resize() 方法会重新计算每个元素的 hash 值,将元素重新放入新的位置,并更新下次扩容的阈值(threshold 成员变量)为原来阈值的 2 倍。初始扩容阈值 threshold = loadFactor * 数组的长度。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

    // 将节点加入 HashMap 集合

    ++modCount;
    if (++size > threshold) {
        // 执行扩容操作
        resize();
    }
    afterNodeInsertion(evict);
    return null;
}

HashMap 的查找操作

当调用 HashMap 的 get() 方法时,get() 方法的处理逻辑如下:

  • 首先,它会根据传入的 key 计算出 hash 值;然后根据计算出的 hash 值计算出 key 对应的数组索引 i
  • 计算出 key 对应的数组索引 i 之后,根据存储位置,从数组中取出对应的 Entry,然后通过 key 对象的 equals() 方法判断传入的 key 和 Entry 中的 key 是否相等:
    • 如果传入的 key 和 Entry 中的 key 相等,则查找操作完成,返回 Entry 中的 value;
    • 如果传入的 key 和 Entry 中的 key 不相等,判断数组在索引 i 上的结构是链表 还是 红黑树,然后调用相应的查找数据的方法。直到找到相等的 Entry 或者没有下一个 Entry 为止。

自定义类型作为 Map 的 key,注意事项

面试中如何通过 HashMap 展示你在数据结构方面的功底?-极客时间 (geekbang.org)

当 HashMap 的 key 为自定义类型时,我们需要重写(Override)该类的 equals() 方法和 hashCode() 方法。因为:

  • 重写 equals() 方法的原因:HashMap 的查找操作需要使用 key 对象的 equals() 方法判断传入的 key 和 Entry 中的 key 是否相等。我们需要保证逻辑上相同的对象,使用 equals() 方法判断时结果为 true。
  • 重写 hashCode() 方法的原因:HashMap 在使用哈希函数计算 key 的 hash 值时,需要使用 key 对象的 hashCode() 方法。我们需要保证逻辑上相同的对象,hashCode() 方法的返回值也相同。

HashMap 的容量大小问题

HashMap 的数组长度总是为 2 的幂次方。不论传入的初始容量是否为 2 的幂次方,最终都会转化为 2 的幂次方。

HashMap 中根据 key 计算出 hash 值,然后根据计算出的 hash 值计算出 key 对应的数组索引 i 时,通过 hash 值 和 数组的长度 - 1 做与运算获得 key 对应的数组索引 i ,即 i = hash & (n - 1)


HashMap 设计的非常巧妙:

  • 在计算 hash 值时,它先将 key 的 hashCode 值无符号右移 16 位,然后再和 key 的 hashCode 值做 异或 运算,使 key 的 hashCode 值高 16 位的变化映射到低 16 位中,使 hashCode 值高 16 位也参与后续索引 i 的计算(i = hash & (n - 1))。减少了碰撞的可能性。
  • 在根据 hash 值计算 key 对应的数组索引 i 时,它将 hash 值 和 数组的长度 - 1 做与运算获得 key 对应的数组索引 i,即 i = hash & (n - 1)。由于数组的长度 n 是 2 的幂次方,n - 1 可以保证它的二进制的后几位都是 1,n 的这一位及之前的位都是 0。因此,计算出的数组索引 i 和 hash 值的二进制表示中后几位有关,而与前面的二进制位无关
  • 当 b 是 2 的幂次方时,a % b == a & (b - 1)。CPU 处理位运算比处理数学运算的速度更快,效率更高。

HashMap 的死循环问题

HashMap 的死循环问题说的是,多个线程同时操作一个 HashMap,当 HashMap 中的键值对数量达到一定程度需要进行扩容操作时,HashMap 有可能会进入一个无限循环,导致程序无法正常执行。

这是因为多个线程同时操作一个 HashMap,多个线程调用 HashMap 的 resize() 执行扩容操作,HashMap 中的链表有可能成环,程序无法从遍历链表中退出,从而导致程序进入死循环。

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

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

相关文章

深入理解ConcurrentHashMap1.8源码

1. 概述 之前介绍了ConcurrentHashMap1.7,采用的是数组分段锁的方式来实现的。虽然说采用分段锁的方式能够在一定程度上提高并发的效率,但是锁的粒度是Segment级别的,其实还是挺大的。 于是,ConcurrentHashMap1.8继续在1.7版本上…

postgresql_internals-14 学习笔记(五)Buffer Cache

新年的第一篇博客~ 一、 Buffer Cache简介 1. 主要用途 调和内存(ns级)与磁盘(ms级)间的速度差异。 pg不仅用自己的buffer cache,也用os cache,所以它使用了“双缓存”,这也是很多文档推荐sha…

《UEFI内核导读》UEFI是不是操作系统?

敬请关注微信公众号:“固件C字营” 最近一直在思考一个问题,UEFI是什么?UEFI算不算是操作系统? 众所周知,计算机系统是由软件和硬件两大部分组成的,但从更科学的角度来划分,我们其实可以分得更…

电子学会2020年9月青少年软件编程(图形化)等级考试试卷(四级)答案解析

目录 一、单选题(共15题,每题2分,共30分) 二、判断题(共10题,每题2分,共20分) 三、编程题【该题由测评师线下评分】(共5题,共50分) 青少年软件…

【MySQL】说透锁机制(三)行锁升表锁如何避免? 锁表了如何排查?

文章目录前言哪些场景会造成行锁升表锁?如何避免?如何分析排查?查看InnoDB_row_lock%相关变量查看 INFORMATION_SCHEMA系统库总结最后前言 在上文我们曾小小的提到过,在索引失效的情况下,MySQL会把所有聚集索引记录和间隙都锁上&#xff0…

【正点原子】嵌入式Linux C应用编程-第十一章

第十一章:线程 前言: 与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元为线程,而不是进程 1:线程的概念 什么是线程&#xff…

【游戏编程扯淡精粹】自研引擎切 UE

【游戏编程扯淡精粹】自研引擎切 UE UF2022 的两篇讲座,再加上 The Machinery 引擎项目失败 结合过去两年笔者使用自研引擎的体验,其实有一部分是共通的 Crystal Dynamics:如何从自研引擎转变到虚幻引擎5 游戏技术(featurelist…

LVGL的学习及使用

1、LVGL简介 LVGL是最受欢迎的免费开源嵌入式图形库,可为任何MCU、MPU和显示器类型创建漂亮的用户界面。使用SquareLine工作室,使用拖放UI编辑器来简化开发。 1.1、LVGL源码下载 lvgl 在github 上的开源代码 https://github.com/lvgl/lvgl 下载的源码包里…

Vue的数据绑定

一、Vue的数据绑定 1、单向数据绑定:将Model绑定到View上,当通过JavaScript代码改变了Model时,View就会自动刷新。不需要进行额外的DOM 操作就可以实现视图和模型的联动 ​ a、数据只保存一份 ​ b、data—->DOM ​ (1&am…

在wsl下开发T113的主线linux(3)-写入spinand测试

接下来是烧写入硬件验证,我的板子焊接的是W25N01GV,这里使用xfel,因为支持写入spi-nand。GitHub - xboot/xfel: Tiny FEL tools for allwinner SOC, support RISC-V D1 chipTiny FEL tools for allwinner SOC, support RISC-V D1 chip - GitH…

QML学习笔记【04】:常用控件

1 Repeater与model Window {width: 640; height: 480visible: truetitle: qsTr("Hello World")Column{id: colspacing: 30Repeater{model: 3 //model控制了所有的数据,这里定义了Button的数量Button{width: 100; height: 50text: "btn" index…

整数划分——完全背包的变形

整数划分——完全背包的变形一、题目二、思路分析1、状态转移方程(1)状态表示(2)方程书写2、循环与初始化(1)循环(2)初始化三、代码一、题目 二、思路分析 这道题这么看的话还是比较…

Docsify使用之Markdown语法

Docsify使用过程中的排版,他是基于Markdown语法的。我们来看一下使用的常用语法: 字体加粗: 在需要加粗的文字前后各加两个** 具体格式如下 **加粗内容** 在需要加粗的文字前后各加一个* 具体格式如下 *倾斜内容* 在需要加粗并且倾斜的…

缅怀2022,展望2023

个人主页:董哥聊技术我是董哥,嵌入式领域新星创作者创作理念:专注分享高质量嵌入式文章,让大家读有所得!文章目录1、缘起2、收获3、憧憬不知不觉,2022已然到了最后一天,同时也是我技术创作一周年…

2.脚手架和逆向工程-使用renren开源

1.脚手架工程 脚手架工程提供了业务模块通用的类,比如返回结果封装、异常封装、分页工具类等 比较好用脚手架工程如renren-fast 备份地址 gitgithub.com:nome1024/renren-fast.git 2.逆向工程——使用renren-generator生成代码 逆向工程的作用是根据数据库快速生…

2022年仪器仪表行业研究报

第一章 行业概况 仪器仪表是用以检出、测量、观察、计算各种物理量、物质成分、物性参数等的器具或设备。真空检漏仪、压力表、测长仪、显微镜、乘法器等均属于仪器仪表。仪器仪表是人们对客观世界的各种信息进行测量、采集、分析与控制的手段和设备,是人类了解世界…

MySQL之表的修改和约束条件的添加

修改表中的数据:update[DML] 语法格式: update 表名 set 字段名1值1,字段名2值2,字段名3值3......where 条件;注意:没有条件限制会导致所有数据全部更新 举例: - 将id号为10的学生的姓名改变为"jas…

【自学Python】解释型程序与编译型程序

解释型程序与编译型程序 解释型程序与编译型程序教程 高级语言所编制的程序不能直接被计算机识别,必须经过转换才能被执行,按转换方式可将它们分为两类:解释型程序与编译型程序。 解释型程序 执行方式类似于我们日常生活中的 “同声翻译”…

Vue--》实现todo-list组件的封装与使用

目录 项目结构 创建todolist组件 创建todoinput组件 创建todobutton组件 项目结构 今天用 vite 脚手架搭建一个 vue3 的小案例,vite的搭建过程参考:vite的搭建 。其项目结构组件构成如下:注意:因为使用的是 vite 框架&#x…

51单片机GMS短信自动存取快递柜

实践制作DIY- GC0103-直流电机PID速度控制 一、功能说明: 基于51单片机设计-GMS短信自动存取快递柜 功能介绍: STC89C52RC最小系统板0.96寸OLED显示器DY-SV17F串口语音播报模块4*4矩阵键盘GSM短信模块4路舵机(模拟4个柜子) ***…