首页 > 编程语言 >Java HashMap 详解

Java HashMap 详解

时间:2024-02-29 15:02:14浏览次数:28  
标签:Java HashMap 元素 哈希 链表 详解 数组 hash

HashMap

HashMap 继承自 AbstractMap,实现了 Map 接口,基于哈希表实现,元素以键值对的方式存储,允许键和值为 null。因为 key 不允许重复,因此只能有一个键为 null。HashMap 不能保证放入元素的顺序,它是无序的,和放入的顺序并不相同。HashMap 是线程不安全的。

1. 哈希表

哈希表基于数组实现,当前元素的关键字通过某个哈希函数得到一个哈希值,这个哈希值映射到数组中的某个位置。哈希函数的好坏直接决定该哈希表的性能

当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是所谓的哈希冲突,也叫哈希碰撞

解决方法如下:

  • 开放定址法:当冲突发生时,使用某种探查技术在散列表中形成一个探查序列,沿此序列逐个单元地查找,直到碰到一个开放的地址(即该地址单元为空),将待插入的新结点存入该地址单元
  • 链地址法:可将散列表定义为一个由 m 个头指针组成的指针数组,将所有关键字为同义词的结点链接在同一个单链表中,初始时数组中各分量的初值应均为 1
  • 再哈希法:同时构造多个不同的哈希函数,发生冲突时再换别的哈希函数

2. JDK1.7 实现原理

HashMap 由数组和链表实现对数据的存储,HashMap 里面实现一个静态内部类 Entry,包含 Key、Value 和对 key 的 hashcode 值进行 hash 运算后得到的 Hash 值,它还具有 Next 指针,可以连接下一个 Entry 实体,以此来解决 Hash 冲突的问题

3. JDK1.7 存储流程

  • 初始化哈希表:真正初始化哈希表(初始化存储数组)是在第一次添加键值对时
    • 数组为空:设置默认阈值与初始容量
    • 设置了传入容量:将传入的容量大小转化为大于自身的最小的二次幂。如果超出最大允许容量,则设置为最大值
  • 判断键是否为空:对 null 作哈希运算,结果为 0,所以以 null 为键的键值对一般放在数组首位,该位置的新值总是会覆盖旧值
  • 计算元素存放位置:首先根据 key 的 hashcode 计算 hash 值,然后根据 hash 值计算 index 下标值
    • 哈希冲突:当发生哈希冲突时,为了保证键的唯一性,哈希表不会马上在链表中插入新数据,而是先遍历链表,查找该键是否已存在,若已存在,替换即可
  • 添加键值对:使用头插法,新添加元素放在链表头部,原始节点作为新节点的后继节点

4. JDK1.7 哈希函数

JDK 1.7 做了 9 次扰动处理 = 4 次位运算 + 5 次异或运算

5. JDK1.7 下标计算

计算元素位置采用的是 & 运算,该方法返回 h & (length - 1),其中 h 为 key 的 hash 值,length 是数组长度

6. JDK1.7 扩容机制

先判断是否需要扩容,再插入

7. JDK1.8 实现原理

1.8 以前 HashMap 采用 数组 + 链表 实现,即使用链表处理冲突,同一 hash 值的节点都存储在一个链表里。但是当同一 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。JDK1.8 中,HashMap 采用 数组 + 链表 + 红黑树 实现,当链表长度超过阈值时,将链表转换为红黑树,大大减少了查找时间

8. JDK1.8 存储流程

  • 初始化哈希表:真正初始化哈希表(初始化存储数组)是在第一次添加键值对时
    • 数组为空:设置默认阈值与初始容量
    • 设置了传入容量:将传入的容量大小转化为大于自身的最小的二次幂。如果超出最大允许容量,则设置为最大值
  • 判断键是否为空:对 null 作哈希运算,结果为 0,所以以 null 为键的键值对一般放在数组首位,该位置的新值总是会覆盖旧值
  • 计算元素存放位置:首先根据 key 的 hashcode 计算 hash 值,然后根据 hash 值计算 index 下标值
    • 哈希冲突:当发生哈希冲突时,为了保证键的唯一性,哈希表不会马上在链表中插入新数据,而是先遍历链表,查找该键是否已存在,若已存在,替换即可;如果不存在,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8 并且数组长度大于 64, 大于的话链表转换为红黑树
  • 添加键值对:链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后

9. JDK1.8 哈希函数

JDK 1.8 简化了扰动函数 = 只做了 2 次扰动 = 1 次位运算 + 1 次异或运算,本质是哈希码的低 16 位异或高 16 位

10. JDK1.8 下标计算

计算元素位置采用的是 & 运算,该方法返回 h & (length - 1),其中 h 为 key 的 hash 值,length 是数组长度

11. JDK1.8 扩容机制

先进行插入,插入完成再判断是否需要扩容。扩容时,1.7 需要对原数组中的元素进行重新 hash 定位,以确定在新数组中的位置,1.8 采用更简单的判断逻辑,位置不变或索引 + 旧容量大小


相关问题

1. 扩容机制?

HashMap 使用懒扩容机制,只有在进行 PUT 操作时才会判断是否扩容,需要用到的属性有两个:

  • 阈值:threshold,初始容量为 16,扩容时需要使用
  • 负载因子:loadFactor,默认是 0.75,用于减缓哈希冲突,如果等到数组满了才扩容,那是某些桶可能就不止一个元素了

阈值 = 数组大小 * 负载因子,容器默认大小为 16,此时 阈值 = 16 * 0.75 = 12,如果当前数组中元素的数量大于阈值,则将数组大小扩大为原来的两倍,并将原来数组中的元素进行重新放到新数组中。需要注意的是,每次扩容之后,都要重新计算元素在数组的位置,因为元素所在位置和数组长度有关,既然扩容后数组长度发生了变化,那么元素位置也会发生变化

2. 针对扩容机制的优化方案?

我们可以自定义数组容量及加载因子的大小。加载因子过大时,HashMap 内的数组使用率高,内部极有可能形成 Entry 链,影响查找速度。加载因子过小时,HashMap 内的数组使用率低,内部不会生成 Entry 链,或者生成的 Entry 链很短,提高了查找速度,不过会占用更多的内存。所以要进行时间和空间的折中考虑

3. 为什么不直接使用 hashcode 作为存储数组的下标位置?

因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为非常大,前后加起来大概 40 亿的映射空间,一个 40 亿长度的数组,内存是放不下的。而且使用之前还需要对数组的长度取模运算,得到余数才能用来访问数组下标

4. 为什么要作扰动处理?

加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少哈希冲突

5. 为什么采用(哈希码 & 数组长度减一)这种方式?

这也解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为 数组长度 减一 正好相当于一个低位掩码。与操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,其结果与取模运算相同,效率却要高很多

6. 为什么在 1.8 使用尾插法插入新结点?

因为 1.7 扩容时,元素会被重新移动到新的数组,而使用头插法会使链表发生反转,比如原本是 A-B-C 的链表,扩容之后就变成 C-B-A 了,在多线程环境下,会导致链表成环的问题。而尾插法,在扩容时会保持链表原本的顺序不变,就不会出现链表成环的问题

标签:Java,HashMap,元素,哈希,链表,详解,数组,hash
From: https://www.cnblogs.com/Yee-Q/p/18040854

相关文章

  • javascript中的var,let,const区别
    const:这个最简单,只需记住是声明的常量,定义的时候必须声明const的具体值,且之后不允许改变const的值 var和let区别1、由于js引擎存在预解析,会把var变量名进行提升对于var来说是这样执行的varm;console.log(m);m=10;let不存在变量提升,会直接报错   2、var是全局......
  • 2.28继续javaweb
     今天继续昨天没有完成的内容@Data@AllArgsConstructor@NoArgsConstructorpublicclassPlan{privateStringname;privateintnumber;privateStringsum;privateStringidea;privateStringenglish;privateStringmath;privateLocalDateT......
  • 扣子(coze.cn)| 由浅入深,手把手带你实现Java转型学习助手
    扣子(coze.cn)是一款用来开发新一代AIChatBot的应用编辑平台,无论你是否有编程基础,都可以通过这个平台来快速创建各种类型的ChatBot,并将其发布到各类社交平台和通讯软件上!2月1日,扣子国内版已经正式上线啦~赶快来体验一下吧!一转眼,ChatGPT已经在AI界炙手可热超过一年,堪称新晋......
  • ConcurrentHashMap 核心源码解析
    废话不多说,直接看代码类名与HashMap很相似,数组、链表结构几乎相同,都实现了Map接口,继承了AbstractMap抽象类,大多数的方法也都是相同的publicclassConcurrentHashMap<K,V>extendsAbstractMap<K,V>implementsConcurrentMap<K,V>,Serializable核心方法Node方法......
  • 「java.util.concurrent并发包」之 Semaphore
    一Semaphore是什么Semaphore也叫信号量,在JDK1.5被引入,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0......
  • java向上转型和向下转型
    1.问题向上转型的意义是什么?向下转型又有什么条件?2.解决参考:聊聊java的向上转型与向下转型向上转型向上转型是用来表现新类和基类之间的关系。在传统中,由导出类转型成基类,在继承图中是向上移动的。因此称作向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总......
  • JAVA虚拟机系列: (一) . JDK1.6/ 1.7/ 1.8运行时内存分配简要图解
     注意:  1.本文讨论均为JDK官方版本,默认采用的HotSpot虚拟机;  2.图片为本人绘制,转载请标明出处;  3.本博均为个人理解,如有分歧,欢迎指正和讨论 从JDK1.6到1.8,运行时内存分配简图分别如下:  在JDK1.7中的HotSpot中,已经把原本放在方法区的字......
  • java参数 -xms -xmx
    参考链接:https://blog.csdn.net/lgleje/article/details/125041480xms、xmx-xms:设置初始化堆内存大小,默认2M-xmx:设置最大可分配堆内存大小,默认64M示例:#初始化128MB堆内存,允许最大堆内存最大1024MB.java-Xms128m-Xmx1024m#初始化256MB堆内存,允许最大......
  • 机器学习策略篇:详解满足和优化指标(Satisficing and optimizing metrics)
    满足和优化指标要把顾及到的所有事情组合成单实数评估指标有时并不容易,在那些情况里,发现有时候设立满足和优化指标是很重要的,让我告诉是什么意思吧。假设已经决定很看重猫分类器的分类准确度,这可以是\(F_1\)分数或者用其他衡量准确度的指标。但除了准确度之外,还需要考虑运行时......
  • 记录 Ubuntu20.04 配置 vscode/gcc/g++ 和 java17
    换源问题在网上找的教程,基本都是安装好Ubuntu后立刻更换软件下载源,但20.04版本我换源之后非常慢,并且后续安装软件时出现依赖问题无法解决等等,我试了清华源和自动选择最佳服务器都不行,最后只能重装。vscode参考:Ubuntu20.04下安装VSCode(配置C/C++开发环境)建议用sudosnapinstal......