HashMap 是日常开发中,用的最多的集合类之一,也是面试中经常被问到的 Java 类之一。同时,HashMap 在实现方式上面又有十分典型的范例。不管是从哪一方面来看,学习 HashMap 都可以说是有利无害的。

分析 HashMap 的源码的文章在网上面已经数不胜数了,本文另辟蹊径来分析 HashMap 的设计思想。

底层数据结构

说到 HashMap 的数据库,我们需要从两个 JDK 版本来分析:JDK7JDK8

JDK7 版本的 HashMap 的数据结构为:数组 + 链表。而 JDK8  版本的 HashMap 的数据结构为:数组 + 链表 + 红黑树。可以看到 7 和 8 中 HashMap 的底层数据结构最主要的区别就是 Java8 多了红黑树。

为何是数组加链表

上文中说到了 不管是 7 或者8 ,底层数据结构都是 数组 + 链表,但这又是为什么呢?

数组是一个链式数据结构。put时,通过特定的哈希算法将key映射成数组下标,这样子就可以将数据保存在对应的槽中,这个槽在 HashMap 中被称为 Entry。在 get 时候,通过相同的哈希函数,将 key 进行哈希运算,可以得到对应的下标,就可以快速找到该 key 对应的 value。这时候, get 的时间复杂度还是 O(1)。

但,哈希算法就避免不了有哈希冲突,不同的值通过哈希运算之后可能得到同一个值。在散列表的相关概念中,介绍了几种解决哈希冲突的方案,而HashMap就是使用的链表法。

在发生了哈希冲突之后,我们在Entry中形成一个单链表。但是这里还存在了一个问题,如果链表过长,检索起来的效率同样也会很低。于是,在 Java8 中,通过链表转红黑树来解决这个问题。

为何要加上红黑树

为什么要链表转红黑树,我们需要从数据结构来解析。

如果从一个无序单链表中检索数据,我们只能从头到尾一个一个检索,一旦数据量很大的情况下,检索的效率就很低(O(n))。这时,我们想到了红黑树,从目前的情况来看,红黑树能很好地解决这个问题(时间复杂度度为O(logn))。

我们先来看看红黑树的定义:

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

红黑树

要是红黑树,首先得是二叉查找树:

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;

简单总结一下,红黑树的左节点要比父节点小,右节点要比父节点大。如果要检索一个数字,可以将时间复杂度从 O(n) 降低到 O(logn)。

当然了,添加了红黑树的数据结构之后,代码实现要比 只用数组 + 链表要复杂了好几倍。看代码的时候简直是不能再痛苦了。

什么时候转成红黑树,有什么转成链表

在源码中有这么一个字段,static final int TREEIFY_THRESHOLD = 8;,见字知义,这个字段的意思链表转红黑树的阈值,也就是 8。同样的,还有这么一个字段,static final int UNTREEIFY_THRESHOLD = 6;,它意思是红黑树转链表的阈值。

这里还需要再注意一个字段static final int MIN_TREEIFY_CAPACITY = 64。这个字段意思是链表转红黑树的最小数组大小,也就是说只有在 **链表长度大于8且数组长度大于64的时候,链表才会转成红黑树。**但在本节这不是重点,也就不多做阐述,大家只需要知道这肯定是为了性能就是了。

为什么是 8 呢?在源码的注释中也有解释,英文翻译过来就是下面的意思。

链表查询的时间复杂度是 O (n),红黑树的查询复杂度是 O (log n)。在链表数据不多的时候,使用链表进行遍历也比较快,只有当链表数据比较多的时候,才会转化成红黑树,但红黑树需要的占用空间是链表的 2 倍,考虑到转化时间和空间损耗,所以我们需要定义出转化的边界值。

在考虑设计 8 这个值的时候,我们参考了泊松分布概率函数,由泊松分布中得出结论,链表各个长度的命中概率为:

* 0:    0.60653066* 1:    0.30326533* 2:    0.07581633* 3:    0.01263606* 4:    0.00157952* 5:    0.00015795* 6:    0.00001316* 7:    0.00000094* 8:    0.00000006

意思是,当链表的长度是 8 的时候,出现的概率是 0.00000006,不到千万分之一,所以说正常情况下,链表的长度不可能到达 8 ,而一旦到达 8 时,肯定是 hash 算法出了问题,所以在这种情况下,为了让 HashMap 仍然有较高的查询性能,所以让链表转化成红黑树,我们正常写代码,使用 HashMap 时,几乎不会碰到链表转化成红黑树的情况,毕竟概念只有千万分之一。

为什么两个阈值不一样的,大家想想,如果一样的,在链表达到8 的时候,会转成红黑树,但红黑树转链表的阈值也是8,这时候就会出现循环转换。

扩容的条件

对比 HashMap 在 7 和 8 中初始化,我发现两个版本的初始化做的事情并不一样。

在 Java 7 中,HashMap 初始化的时候,会有个默认容量 (16)。但在 Java8 中,HashMap 初始化的时候,默认容量为0,只有在第一次 put 的时候,才会扩容到 16。

在 HashMap 源码中,有一个字段定义 static final float DEFAULT_LOAD_FACTOR = 0.75f;。这个字段的意思是,当HashMap 的长度 = HashMap 当前容量 * 0.75的时候,就会发生扩容。

关于为什么负载因子是0.75,我们可以在源码注释找到一定的答案。

load factor

大致意思就是说负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

最后,我们要知道HashMap的扩容是变成原先容量的 2 倍。

Hash函数

我们来看看 Java 8 的 hash 函数。

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

这里的大概意思就是,先计算出 key 的 hashCode h。然后计算计算 h ^ (h >>> 16)。无符号右移16位。这么做的好处是使大多数场景下,算出来的 hash 值比较分散。

一般来说,hash 值算出来之后,要计算当前 key 在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小,这样做的好处,就是可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上,但取模操作对于处理器的计算是比较慢的,数学上有个公式,当 b 是 2 的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为:(n-1) & hash。

此问题可以延伸出三个小问题:

  1. 为什么不用 key % 数组大小,而是需要用 key 的 hash 值 % 数组大小。

    如果 key 是数字,直接用 key % 数组大小是完全没有问题的,但我们的 key 还有可能是字符串,是复杂对象,这时候用 字符串或复杂对象 % 数组大小是不行的,所以需要先计算出 key 的 hash 值。

  2. 计算 hash 值时,为什么需要右移 16 位?

    hash 算法是 h ^ (h >>> 16),为了使计算出的 hash 值更分散,所以选择先将 h 无符号右移 16 位,然后再于 h 异或时,就能达到 h 的高 16 位和低 16 位都能参与计算,减少了碰撞的可能性。

  3. 为什么把取模操作换成了 & 操作?

    key.hashCode() 算出来的 hash 值还不是数组的索引下标,为了随机的计算出索引的下表位置,我们还会用 hash 值和数组大小进行取模,这样子计算出来的索引下标比较均匀分布。

    取模操作处理器计算比较慢,处理器对 & 操作就比较擅长,换成了 & 操作,是有数学上证明的支撑,为了提高了处理器处理的速度。

hash 冲突时怎么办?

hash 冲突指的是 key 值的 hashcode 计算相同,但 key 值不同的情况。

如果桶中元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部;

如果桶中元素已经是链表,并且链表个数大于等于 8 时,此时有两种情况:

  1. 如果此时数组大小小于 64,数组再次扩容,链表不会转化成红黑树;
  2. 如果数组大小大于 64 时,链表就会转化成红黑树。

这里不仅仅判断链表个数大于等于 8,还判断了数组大小,数组容量小于 64 没有立即转化的原因,猜测主要是因为红黑树占用的空间比链表大很多,转化也比较耗时,所以数组容量小的情况下冲突严重,我们可以先尝试扩容,看看能否通过扩容来解决冲突的问题。

hashmap containsvalue时间复杂度_不看看HashMap源码,怎么和面试官谈薪资相关推荐

  1. hashmap containsvalue时间复杂度_面试宝典:数据结构HashMap

    常用数据结构在新增.查找等基础操作上的性能 1.数组 采用一段连续的存储单元来存储数据 对于指定下标的查找,时间复杂度为O(1) 通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复 ...

  2. SpringCloud源码学习(二) 面试官问我Eurake服务注册的实现细节?

    上期我们大概得了解了Eurake向Spring上下文中注册EurekaServiceRegistry和EurekaRegistration的详细过程,其中总调度类EurekaAutoServiceRe ...

  3. 获取arraylist的长度_啃碎JDK源码(三):ArrayList

    前言 很久之前写过一篇有关HashMap的文章:啃碎JDK源码(四):HashMap,反响不错.本来手后面是想写篇文章来介绍ArrayList,后来事情多就忘了,今天就来好好聊聊ArrayList. ...

  4. 羊了个羊游戏网站源码_带前后端源码,带教程

    羊了个羊游戏源码_带前后端源码_及配置教程说明 这是啥游戏?据悉,这是一款卡通背景的消除闯关游戏.玩家们需要点击上方卡牌,被选中的卡牌会下移到底部的木框中,框内最多可以储存7张卡牌,当有3张相同的卡牌 ...

  5. 小肩膀POST+JS第三期(价值1000元)_高清带源码

    小肩膀POST+JS第三期(价值1000元)_高清带源码 .课程整体介绍 2.谷歌开发者工具 3.火狐firebug组件 4.火狐开发者工具 5.HTTP Analyzer V7 6.Fiddler ...

  6. 为什么下载源码包需要到官网上去下载?

    1. 为什么下载源码包需要到官网上去下载? 答:官网提供的源码包更完整,无修改,无病毒,无后门. 2. 64位机器是否可以安装32位rpm包?64位机器是否可以安装32位的mysql二进制免编译包? ...

  7. dubbo源码解析之框架粗谈

    dubbo框架设计 一.dubbo框架整体设计 二.各层说明 三.dubbo工程模块分包 四.依赖关系 五.调用链 文章系列 [一.dubbo源码解析之框架粗谈] [二.dubbo源码解析之dubbo ...

  8. hashmap containsvalue时间复杂度_Java-HashMap面试问答

    结构.null键null值 HashMap是基于Hash表对Map接口的实现,HashMap具有较快的访问速度,但是遍历的顺序却是不确定的,HashMap提供所有可选的映射操作,并允许使用null键和 ...

  9. 手撕HashMap数据结构(带你逐行阅读源码)

    目录 概述 原理简述 新旧版本对比 结构设计原理 继承关系 成员变量 核心:为什么负载因子设定为0.75? 核心:为什么树化的链表阈值是8? 核心:为什么树退化的链表阈值是6? 构造函数 默认构造方法 ...

最新文章

  1. Android中对Log日志文件的分析[转]
  2. 从创业到成功,SaaS巨头Salesforce靠的是这七大秘诀
  3. Intel发布神经网络压缩库Distiller:快速利用前沿算法压缩PyTorch模型
  4. .NET Framework、C#语言、IDE、CLR 版本历史及其差异(最新)
  5. html table没有align,HTML
  6. 【典型错误】The type java.lang.Object cannot be resolved.
  7. C语言的单向链表的操作方法
  8. 一次有趣的面试经历,当前端面试碰到后端面试官会发生什么?
  9. 使用SQL Server Management Studio 创建数据库备份作业
  10. 使用maven启动web项目报错
  11. ecshop 影响全局的标量lib_main.php
  12. Disruptor 极速体验
  13. 缓存框架 EhCache 使用 3
  14. DSP降噪拾音器 语音增强 噪声抑制器 反馈抑制器
  15. 新浪云存储 php,【PHP】新浪云SAE平台将本地图片和远程图片保存至Storage
  16. 【计算机网络】计算机网络的体系结构
  17. httpwatch使用_使用PHP自动化HTTPWatch
  18. 3.3.6 Binomial Trees
  19. 《第五项修炼》读书笔记
  20. 设置(改变)eclipse背景颜色

热门文章

  1. 去马赛克神器 JavPlayer TG Modle 最新版
  2. html+css实现响应式布局入门
  3. 精通ASP.NET MVC ——控制器可扩展性
  4. 如下为利用Linux内核链表创建,Linux内核中链表的实现与应用
  5. java js websocket_js+java websocket记录
  6. mysql5.7安装教程centos_MySQL5.7版CentOS系统简易安装教程
  7. micropython用什么编译_micropython开发者指南
  8. python爬取行业数据_用Python进行Web爬取数据
  9. cnpm安装webpack_Webpack(一)介绍
  10. http安全 Java_HTTP通信安全-身份验证 | 字痕随行