Java HashMap 详解
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 详解的更多相关文章
- [Java] HashMap详解
转自:http://alex09.iteye.com/blog/539545 HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 Hash ...
- 【转】 java中HashMap详解
原文网址:http://blog.csdn.net/caihaijiang/article/details/6280251 java中HashMap详解 HashMap 和 HashSet 是 Jav ...
- java中HashMap详解(转)
java中HashMap详解 博客分类: JavaSE Java算法JDK编程生活 HashMap 和 HashSet 是 Java Collection Framework 的两个重要成 ...
- java集合(2)- java中HashMap详解
java中HashMap详解 基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了非同步和允许使用 null 之外,HashMap 类与 H ...
- Java集合详解4:一文读懂HashMap和HashTable的区别以及常见面试题
<Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...
- java集合详解
1.java集合框架的层次结构 Collection接口: Set接口: HashSet具体类 LinkedHashSet具体类 TreeSet具体类 List接口: ArrayList具体类 L ...
- Java Annotation详解 理解和使用Annotation
系统中用到了java注解: 查了一下如何使用注解,到底注解是什么: (1)创建方法:MsgTrace Java Class==> 在Create New Class中: name:输入MsgTr ...
- 【Java入门提高篇】Day34 Java容器类详解(十五)WeakHashMap详解
源码详解系列均基于JDK8进行解析 说明 在Java容器详解系列文章的最后,介绍一个相对特殊的成员:WeakHashMap,从名字可以看出它是一个 Map.它的使用上跟HashMap并没有什么区别,所 ...
- Java集合详解6:TreeMap和红黑树
Java集合详解6:TreeMap和红黑树 初识TreeMap 之前的文章讲解了两种Map,分别是HashMap与LinkedHashMap,它们保证了以O(1)的时间复杂度进行增.删.改.查,从存储 ...
- Java集合详解3:Iterator,fail-fast机制与比较器
Java集合详解3:Iterator,fail-fast机制与比较器 今天我们来探索一下LIterator,fail-fast机制与比较器的源码. 具体代码在我的GitHub中可以找到 https:/ ...
随机推荐
- OpenGauss3.1.0 单机版安装部署过程
背景 由易到难 先进行单节点的设置 先说坑 openEuler2203 默认安装了python3.9 但是openGauss里面指代了3.6和3.7 /openGauss/install/om 注意在 ...
- 程序启停分析与进程常用API的使用
进程是程序运行的实例,操作系统为进程分配独立的资源,使之拥有独立的空间,互不干扰. 空间布局 拿c程序来说,其空间布局包括如下几个部分: 数据段(初始化的数据段):例如在函数外的声明,int a = ...
- 高性能MySQL实战(三):性能优化 | 京东物流技术团队
这篇主要介绍对慢 SQL 优化的一些手段,而在讲解具体的优化措施之前,我想先对 EXPLAIN 进行介绍,它是我们在分析查询时必要的操作,理解了它输出结果的内容更有利于我们优化 SQL.为了方便大家的 ...
- MySQL 列操作记录
在 MySQL 中,你可以使用多种命令和语句来执行列操作,包括添加.修改.删除列等.以下是一些与列操作相关的常用 MySQL 命令和语句: 1. 添加列: 添加新列到表格中: ALTER TABLE ...
- uni-app 实现下拉刷新功能
我们在运用uni-app开发小程序或h5时,常常需要页面实现下拉刷新功能. 在 js 中定义 onPullDownRefresh 处理函数(和onLoad等生命周期函数同级),监听该页面用户下拉刷新事 ...
- STM32CubeMX教程27 SDIO - 读写SD卡
1.准备材料 正点原子stm32f407探索者开发板V2.4 STM32CubeMX软件(Version 6.10.0) keil µVision5 IDE(MDK-Arm) ST-LINK/V2驱动 ...
- ActiveReports报表行号
=RunningValue(Fields!字段名称.Value, CountDistinct, "矩表分组名称") RunningValue(Fields!区域.Value, Co ...
- BigDecimal详解和精度问题
JavaGuide :「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识. BigDecimal 是大厂 Java 面试常问的一个知识点. <阿里巴巴 Java 开发 ...
- 基于无监督训练SimCSE+In-batch Negatives策略有监督训练的语义索引召回
基于无监督训练SimCSE+In-batch Negatives策略有监督训练的语义索引召回 语义索引(可通俗理解为向量索引)技术是搜索引擎.推荐系统.广告系统在召回阶段的核心技术之一.语义索引模型的 ...
- 【二】强化学习之Parl基础命令--PaddlePaddlle及PARL框架{飞桨}
相关文章: [一]飞桨paddle[GPU.CPU]安装以及环境配置+python入门教学 [二]-Parl基础命令 [三]-Notebook.&pdb.ipdb 调试 [四]-强化学习入门简 ...