java中HashMap的设计精妙在哪?

java,hashmap,设计,精妙 · 浏览次数 : 107

小编点评

## 摘要 本文结合图解和问题,教你一次性搞定HashMap。 **核心原理:** 1. 获取key的哈希值。 2. 如果空,统一是0。 3. 调用对象的.hashCode()方法,与自己的右移16位进行异或。 4. 遍历内部node数组,检查头节点是否和自己要存放的key完全匹配。 5. 如果匹配,得到需要替换的节点。 6. 头节点和自己要放的key不匹配,则判断一下这个头节点是否是红黑树节点。 7. 如果不是红黑树,则遍历链表,完全匹配就得到需要替换的节点。 8. 如果到尾部了,也没匹配的,则插入新节点。 9. 如果前面找到了要替换的节点,则判断一下是否可以替换(是否没要求putIfAbsent,或者value为null)。 10. 如果前面是插入了新节点,非替换,则要modCount++(方便迭代器确认map是否更新),同时++size, 然后和扩容阈值做判断, 如果太大,就resize进行扩容hashMap的扩容过程。 **java8改版:** 1. 改成尾插法,避免频繁的哈希计算和搬移过程。 2. 使用尾插法在链表上插入节点,保证顺序正确。 3. 避免每个点rehash放置,而是最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标. 避免了频繁的哈希计算和搬移过程。 **为什么java8要改成尾插法?** 1. 多线程时,java7的map-put可能造成死循环。 2. 线程扩容到那一半, 还处在遍历链表做头插法搬移的过程时,存了2个局部变量,当前链点now指向a, next指向b,正准备搬移(a->b->c这样的链表,a是头节点) **总结:** 1. HashMap负载因子 LoadFactor,默认值为0.75f。 2. 衡量HashMap是否进行Resize的条件如下:HashMap.Size >= Capacity * LoadFactor。 3. 针对HashMap的性能问题,java8里改成尾插法,可以避免频繁的哈希计算和搬移过程,提高效率。

正文

摘要:本文结合图解和问题,教你一次性搞定HashMap

本文分享自华为云社区《java中HashMap的设计精妙在哪?用图解和几个问题教你一次性搞定HashMap》,作者:breakDawn。

HashMap核心原理

HashMap完整的put过程

以下是对上图的详细解释:

  1. 首先,要获取key的哈希值。
    如果为空,就统一是0
    否则,调用对象的.hashCode()方法,接着再与自己的右移16位进行异或,以便充分利用高位信息。
  2. 接着判断内部node数组是否为空,如果是,先进行初始化扩容。默认为16。
  3. 根据(n-1)&hash值,获取哈希表索引位置。
  4. 哈希表的node数组中,存放的是每组链表的头节点。
    先检查头节点是否和自己要存放的key完全匹配 (hash值相同,key值相同,先hash再key,是因为hash的判断简单,key的equals判断可能会复杂)。如果匹配,得到需要替换的节点。
  5. 头节点和自己要放的key不匹配,则判断一下这个头节点是否是红黑树节点,如果是,说明已经升级成红黑树了,调用putTree插入到红黑树中。
  6. 如果不是红黑树, 那就是遍历链表,完全匹配就得到需要替换的节点。如果到尾部了,也没匹配的,则插入新节点。
  7. 如果前面找到了要替换的节点,则判断一下是否可以替换(是否没要求putIfAbsent,或者value为null),是就替换,不是就结束
  8. 如果前面是插入了新节点,非替换, 则要modCount++(方便迭代器确认map是否更新), 同时++size, 然后和扩容阈值做判断, 如果太大,就resize进行扩容

hashMap的扩容过程,java7和8扩容的区别

java7:

  • 当resize时,新建一个数组newTable
  • 遍历原table中的每个链表和节点,重新hash,找到新的位置放入
  • 放入的方式是头插法,即始终插在链表的头节点。

java8:

  • 不再每个点rehash放置,而是最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标. 避免了频繁的哈希计算和搬移过程。
  • 使用尾插法在链表上插入节点
  • 桶内元素超过8个,链表转成红黑树

为什么java8要改成尾插法?

A:多线程时,java7的map-put可能造成死循环。
A线程扩容到那一半, 还处在遍历链表做头插法搬移的过程时,存了2个局部变量,当前链点now指向a, next指向b,正准备搬移(a->b->c这样的链表,a是头节点)

B线程则同时完成线程扩容,但是map里都是引用,浅拷贝,** 因为是头插法, 会导致顺序变化**, 原本a->b->c 变成了c->b->a。
因此A恢复时, 链点还是a,next还是b, 于是往下走到了b, 取bbs的next时,已经变成了a, 于是发生了a->b->a的循环
导致后续操作的next都是错误操作,引发环形指针。

java8里改成尾插法,这样做resize时,a->b->c 如果仍然哈希到同一个节点, 顺序是不会发生变化的。

虽然解决了死循环问题, 但java8的hashMap仍然是线程不安全的,为什么?

A:因为缺乏同步,导致同节点发生哈希碰撞时,if条件的判断都可能是有问题的,导致本该插在链表头节点后面的,结果直接作为链表头覆盖到数组上了。

具体到底满足什么情况,才会resize扩容呢?

A:HashMap负载因子 LoadFactor,默认值为0.75f。
衡量HashMap是否进行Resize的条件如下:
HashMap.Size >= Capacity * LoadFactor

另一种情况。JDK1.8源码中,执行树形化之前,会先检查数组长度,如果长度小于64,则对数组进行扩容,而不是进行树形化

扩容后,capacity扩容多少倍呢?为什么

A:哈希表每次扩容是两倍。
初始长度为2的幂次方,随后以2倍扩容的方式扩容,元素在新表中的位置要么不动,要么有规律的出现在新表中(二的幂次方偏移量),这样会使扩容的效率大大提高。
另外,hashmap采用二倍扩容还有另外一个好处:可以使元素均匀的散布hashmap中,减少hash碰撞。

 

点击关注,第一时间了解华为云新鲜技术~

与java中HashMap的设计精妙在哪?相似的内容:

java中HashMap的设计精妙在哪?

摘要:本文结合图解和问题,教你一次性搞定HashMap 本文分享自华为云社区《java中HashMap的设计精妙在哪?用图解和几个问题教你一次性搞定HashMap》,作者:breakDawn。 HashMap核心原理 HashMap完整的put过程 以下是对上图的详细解释: 首先,要获取key的哈希

Java交换map的key和value值

在Java中,我们都知道直接交换Map的key和value是不被允许的,因为Map的接口设计是基于key-value对的,其中key是唯一的,并且是不可变的(在HashMap等常见的实现中,虽然key的引用是不可变的,但key对象本身如果是可变的,它的内容是可以变化的,但这样做可能会导致不正确的行为

万字 HashMap 详解,基础(优雅)永不过时

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 在上一篇文章里,我们聊到了散列表的整体设计思想,在后续几篇文章里,我们将以 Java 语言为例,分析标准库中实现的散列表实现,包括 HashMap、ThreadLocalMap、Li

WeakHashMap 和 HashMap 的区别是什么,何时使用?

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 在之前的文章里,我们聊到了 Java 标准库中 HashMap 与 LinkedHashMap 的实现原理。HashMap 是一个标准的散列表数据结构,而 LinkedHashMap

Java进阶:HashMap底层原理(通俗易懂篇)

1.底层结构 Java 7及之前版本 在Java 7及之前的版本中,HashMap的底层数据结构主要是数组加链表。具体实现如下: 数组:HashMap的核心是一个Entry数组(Entry[] table),这个数组的大小总是2的幂。每个数组元素是一个单一的Entry节点,或者是一个链表的

Java中代码Bug记录--泛型失效、数组删除、HashMap死循环

最近在工作的过程中,遇到了不少奇怪自己或者同事的Bug,都是一些出乎意料的,不太容易发现的,记录一下来帮助可能也遇到了这些Bug的人 # 1. 编译时泛型校验失效 ```java Map nameToType = new HashMap(); nameToType.put( "testName",

Java 中的泛型 集合(List,Set) Map

泛型 集合(List,Set) Map 泛型 泛型的本质是参数化类型,即允许在编译时对集合进行类型检查,从而避免安全问题,提高代码的复用性 泛型的具体定义与作用 定义:泛型是一种在编译阶段进行类型检查的机制,它允许在类,方法,接口后通过<> 来声明类型参数.这些参数在编译时会被具体的类型替换.jav

JAVA 中的 StringBuilder 和 StringBuffer 适用的场景是什么?

转自菜鸟教程的一位大哥 未之奋豆 未之奋豆 429***663@qq.com 参考地址 6年前 (2018-05-07) JAVA 中的 StringBuilder 和 StringBuffer 适用的场景是什么? 最简单的回答是,stringbuffer 基本没有适用场景,你应该在所有的情况下选择

Java中可以用的大数据推荐算法

在Java中实现大数据推荐算法时,通常会使用一些开源的机器学习库,如Apache Mahout、Weka、DL4J(DeepLearning4j,用于深度学习)或者Spark MLlib(用于在Spark集群上运行)。由于完整实现一个大数据推荐算法的代码量可能非常大,并且需要配合具体的数据集和环境进

java中SimpleDateFormat解析日期格式的问题

在日常写代码的过程中,我们经常要处理各种格式的日期,常见的日期格式有:“20240601”,“2024-06-01”,“2024-6-1”。如何正确地处理日期格式,尤其是对外接口中参数的日期格式,就很重要了,一个不小心就可能出现意想不到的问题。 举一个我遇到的真实例子:我们提供的对外接口中有一个参数