上篇我们介绍了JDK1.7版的HashMap,今天我们来讲解下JDK1.8版的HashMap。

JDK1.7的实现大家看出有没有需要优化的地方?

其实一个很明显的地方就是:当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

因此JDK1.8 中重点优化了这个查询效率。

1、JDK1.8 HashMap 数据结构图

我们会发现优化的部分就是把链表结构变成了红黑树。原来jdk1.7的优点是增删效率高,于是在jdk1.8的时候,不仅仅增删效率高,而且查找效率也提升了。

注意:不是说变成了红黑树效率就一定提高了,只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树。

问题一:什么是红黑树呢?

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了。

问题二:为什么不一下子把整个链表变为红黑树呢?

这个问题的意思是这样的,就是说我们为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释

(1)构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

2、HashMap构造方法

构造方法一共有四个:

这四个构造方法很明显第四个最麻烦,我们就来分析一下第四个构造方法,其他三个自然而然也就明白了。上面出现了两个新的名词:loadFactor和initialCapacity。我们一个一个来分析:

(1)initialCapacity初始容量

官方要求我们要输入一个2的N次幂的值,比如说2、4、8、16等等这些,但是我们忽然一个不小心,输入了一个20怎么办?没关系,虚拟机会根据你输入的值,找一个离20最近的2的N次幂的值,比如说16离他最近,就取16为初始容量。

(2)loadFactor负载因子

负载因子,默认值是0.75。负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量。 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时索引效率高。

3、put方法

我们在存储一个元素的时候,大多是使用下面的这种方式。

在这里HashMap

,第一个参数是键,第二个参数是值,合起来叫做键值对。存储的时候只需要调用put方法即可。那底层的实现原理是怎么样的呢?这里还是先给出一个流程图:

上面这个流程,不知道你能否看到,红色字迹的是三个判断框,也是转折点,我们使用文字来梳理一下这个流程:

(1)第一步:调用put方法传入键值对

(2)第二步:使用hash算法计算hash值

(3)第三步:根据hash值确定存放的位置,判断是否和其他键值对位置发生了冲突

(4)第四步:若没有发生冲突,直接存放在数组中即可

(5)第五步:若发生了冲突,还要判断此时的数据结构是什么?

(6)第六步:若此时的数据结构是红黑树,那就直接插入红黑树中

(7)第七步:若此时的数据结构是链表,判断插入之后是否大于等于8

(8)第八步:插入之后大于8了,就要先调整为红黑树,在插入

(9)第九步:插入之后不大于8,那么就直接插入到链表尾部即可。

上面就是插入数据的整个流程,光看流程还不行,我们还需要深入到源码中去看看底部是如何按照这个流程写代码的。

鼠标聚焦在put方法上面,按一下F3,我们就能进入put的源码。来看一下:

也就是说,put方法其实调用的是putVal方法。putVal方法有5个参数:

(1)第一个参数hash:调用了hash方法计算hash值

(2)第二个参数key:就是我们传入的key值,也就是例子中的张三

(3)第三个参数value:就是我们传入的value值,也就是例子中的20

(4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值

(5)第五个参数evict :如果为false,那么数组就处于创建模式中,所以一般为true。

知道了这5个参数的含义,我们就进入到这个putVal方法中。

乍一看,这代码完全没有读下去的欲望,第一次看的时候真实恶心到想吐,但是结合上一开始画的流程图再来分析,相信就会好很多。我们把代码进行拆分(整体分了四大部分):

(1)Node

[] tab中tab表示的就是数组。Node

p中p表示的就是当前插入的节点

(2)第一部分:

这一部分表示的意思是如果数组是空的,那么就通过resize方法来创建一个新的数组。在这里resize方法先不说明,在下一小节扩容的时候会提到。

(3)第二部分:

i表示在数组中插入的位置,计算的方式为(n - 1) & hash。在这里需要判断插入的位置是否是冲突的,如果不冲突就直接newNode,插入到数组中即可,这就和流程图中第一个判断框对应了。

如果插入的hash值冲突了,那就转到第三部分,处理冲突

(4)第三部分:

我们会看到,处理冲突还真是麻烦,好在我们对这一部分又进行了划分

a)第三部分第一小节:

在这里判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。

b)第三部分第二小节:

判断插入的数据结构是红黑树还是链表,在这里表示如果是红黑树,那就直接putTreeVal到红黑树中。这就和流程图里面的第二个判断框对应了。

c)第三部分第三小节:

如果数据结构是链表,首先要遍历table数组是否存在,如果不存在直接newNode(hash, key, value, null)。如果存在了直接使用新的value替换掉旧的。

注意一点:不存在并且在链表末尾插入元素的时候,会判断binCount >= TREEIFY_THRESHOLD - 1。也就是判断当前链表的长度是否大于阈值8,如果大于那就会把当前链表转变成红黑树,方法是treeifyBin。这也就和流程图中第三个判断框对应了。

(5)第四部分:

插入成功之后,还要判断一下实际存在的键值对数量size是否大于阈值threshold。如果大于那就开始扩容了。

4、resize方法

为什么扩容呢?很明显就是当前容量不够,也就是put了太多的元素。为此我们还是先给出一个流程图,再来进行分析。

这个扩容就比较简单了,HaspMap扩容就是就是先计算 新的hash表容量和新的容量阀值,然后初始化一个新的hash表,将旧的键值对重新映射在新的hash表里。如果在旧的hash表里涉及到红黑树,那么在映射到新的hash表中还涉及到红黑树的拆分。整个流程也符合我们正常扩容一个容量的过程,我们根据流程图结合代码来分析:

这代码量同样让人恶心,不过我们还是分段来分析:

(1)第一部分:

根据代码也能看明白:首先如果超过了数组的最大容量,那么就直接将阈值设置为整数最大值,然后如果没有超过,那就扩容为原来的2倍,这里要注意是oldThr << 1,移位操作来实现的。

(2)第二部分:

首先第一个else if表示如果阈值已经初始化过了,那就直接使用旧的阈值。然后第二个else表示如果没有初始化,那就初始化一个新的数组容量和新的阈值。

(3)第三部分

第三部分同样也很复杂,就是把旧数据复制到新数组里面。这里面需要注意的有下面几种情况:

A:扩容后,若hash值新增参与运算的位=0,那么元素在扩容后的位置=原始位置

B:扩容后,若hash值新增参与运算的位!=0,那么元素在扩容后的位置=原始位置+扩容后的旧位置。

这里面有一个非常好的设计理念,扩容后长度为原hash表的2倍,于是把hash表分为两半,分为低位和高位,如果能把原链表的键值对, 一半放在低位,一半放在高位,而且是通过e.hash & oldCap == 0来判断,这个判断有什么优点呢?

举个例子:n = 16,二进制为10000,第5位为1,e.hash & oldCap 是否等于0就取决于e.hash第5 位是0还是1,这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位。

5、hash方法

Java 8中的散列值优化函数:

源码中模运算就是把散列值和数组长度做一个"与"操作:

这也正好解释了为什么HashMap的数组长度要取2的整次幂,因为这样(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。

以初始长度16为例,16-1=15,2进制表示是00000000 00000000 00001111,和某散列值做“与”操作如下,结果就是截取了最低的四位值:

但这时候问题就来了:这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重,这时候“扰动函数”的价值就体现出来了:

右位移16位,正好是32位一半,自己的高半区和低半区做异或,就是为了混合原始hashCode的高位和低位,以此来加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来,即降低了哈希冲突的风险又不会带来太大的性能问题。这个设计很巧妙!

6、hash冲突

通过异或运算能够是的计算出来的hash比较均匀,不容易出现冲突。但是偏偏出现了冲突现象,这时候该如何去解决呢?

在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而hashMap中处理hash冲突的方法就是链地址法。

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

7、table数组用transient修饰

从HashMap 的源码,会发现桶数组 table 被申明为 transient。transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。我们再回到源码中,考虑一个问题:桶数组 table 是 HashMap 底层重要的数据结构,不序列化的话,别人还怎么还原呢?

这里简单说明一下吧,HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject两个方法自定义了序列化的内容。这样做是有原因的,试问一句,HashMap 中存储的内容是什么?不用说,大家也知道是键值对。所以只要我们把键值对序列化了,我们就可以根据键值对数据重建 HashMap。有的朋友可能会想,序列化 table 不是可以一步到位,后面直接还原不就行了吗?这样一想,倒也是合理。但序列化 talbe 存在着两个问题:

1)table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间。

2)同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

以上两个问题中,第一个问题比较好理解,第二个问题解释一下。HashMap 的get/put/remove等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。

综上所述,大家应该能明白 HashMap 不序列化 table 的原因了,下面是HashMap自定义的序列化代码:

8、HashMap非线程安全

HashMap源码里面方法是没有synchronized或lock处理的,无法保证线程安全。于是出现了线程安全的ConcurrentHashMap,这个我们后续讲解。

红黑树 键值_大厂面试官:说一下JDK1.8 HashMap有哪些亮点?相关推荐

  1. 红黑树 键值_查找(一)史上最简单清晰的红黑树讲解

    http://blog.csdn.net/yang_yulei/article/details/26066409 查找(一) 我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然 ...

  2. 红黑树 键值_Java集合框架:红黑树概念、插入及旋转操作详细解读就问你会不会...

    初识TreeMap 之前的文章讲解了两种Map,分别是HashMap与LinkedHashMap,它们保证了以O(1)的时间复杂度进行增.删.改.查,从存储角度考虑,这两种数据结构是非常优秀的.另外, ...

  3. 面试项目亮点_大厂面试官常问的亮点我们项目中怎么做出?(软技能)

    这篇作为之前一篇文章的延续,以及对过去3个月我模拟面试过的30多位同学的面试情况总结,我们来聊一下怎么做出亮点 你负责的业务是什么?(学会发现问题) 之前在群里参加活动的同学,有不少说在小公司,被业务 ...

  4. hashmap转红黑树的阈值为8_面试必问的HashMap,一次彻底帮你搞定HashMap源码

    HashMap结构 数组+链表+红黑树 链表大于8转红黑树,红黑树节点数小于6退回链表. 存放的key-value的Node节点 static class Node implements Map.En ...

  5. hashmap存多少条数据_干货 | 面试官想问的HashMap,都在这一篇里面了!

    来源公众号:非科班的科班 本文思维导图 HashMap简介 HashMap 是很常用的一种集合框架,其底层实现方式在 JDK 1.7和 JDK 1.8中却有很大区别.HashMap 是用来存储数据的, ...

  6. hashmap中用红黑树不用其他树_HashMap面试专题:常问六题深入解析

    引言 其实我很早以前就想写一篇关于HashMap的面试专题.对于JAVA求职者来说,HashMap可谓是集合类的重中之重,甚至你在复习的时候,其他集合类都不用看,专攻HashMap即可. 然而,鉴于网 ...

  7. 大厂面试官手把手教你:三步写出好简历

    本文由作者 海贝学姐 发布于社区 最近一个月行业巨变,可能不少同学会有找工作的需求.学姐作为曾经的大厂面试官,经常看到一些不太合格的简历,不少还是工作三五年以上的,很多人是"做得多.写不好& ...

  8. hashmap转红黑树的阈值为8_面试必考的 HashMap,这篇总结到位了

    点击蓝色"JavaKeeper"关注我哟加个"星标",一起成长,做牛逼闪闪的技术人 1 概述 HashMap是基于哈希表实现的,每一个元素是一个key-valu ...

  9. kruskal算法_大厂面试为什么总是考算法?

    很多同学都问过我这个问题,毕竟,在实际工作中,我们近乎根本不可能从底层实现一遍经典算.法如果真的以工作内容为导向,算法还真可能对绝大部分同学来说没什么用.但是,算法却是大厂面试考察的重点.甚至,极端一 ...

最新文章

  1. 首次使用计算机鼠标键盘不能用,解决方案:如何解决无法使用计算机鼠标和键盘的问题?如果启动后无法使用鼠标和键盘该怎么办?...
  2. AI需求强劲 AI芯片市场规模有望达到405亿
  3. Python入门100题 | 第058题
  4. window.setTimeout() 和 window.setInterval() 使用说明
  5. 135_Power Query M语言快捷输入之输入法设置自定义短语
  6. 软考网络管理员学习笔记3之第三章网络体系结构
  7. python dll文件丢失_python34.dll
  8. Pwn2Own黑客大赛战况:iPhone 20秒被黑
  9. 如何关闭父窗体?C#(已解决)
  10. WP7模拟器使用技巧
  11. 【阿里开发规范】Java开发手册(嵩山版)
  12. linux消息队列 性能,消息队列消息总大小的问题
  13. [3] UI原型设计工具Pencil Project 学习系列----- 进阶
  14. ActiveMq的简介
  15. Sky Walking 介绍 及 8.7.0 源码学习环境搭建
  16. html编写扑克牌游戏,用js编写扑克牌小游戏
  17. 移动端字体加粗的解决方案
  18. 农村经济与科技杂志农村经济与科技杂志社农村经济与科技编辑部2022年第9期目录
  19. 支付宝/云闪付个人免签
  20. 21闭关修炼 习题分册

热门文章

  1. solidworks中工程图标注三面焊符号
  2. hive on spark 线上问题排查案例分享
  3. CSDN做测试的老师教你Jmeter生成压力测试报告
  4. 微信群流量池构建出不为人知的“商业帝国”
  5. vue3 pinia 状态管理(清晰明了)
  6. 优漫动游平面设计培训包含什么,平面设计培训内容有哪些?
  7. 图文详解如何从官网下载Hadoop安装包
  8. 支付系统保证可靠性的秘诀 ----- 订单的补偿和补单
  9. 91,bigmeap 功能对比
  10. 偷偷修复漏洞 苹果要求研究员噤声