栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap的原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap的原理

1.HashMap有两个参数会影响其性能,初始容量和加载因子,

2.谈一下HashMap的底层原理是什么?
基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对key做一个hashCode()的计算来得到它在bucket数组中的位置来存储node对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。//get就比较简单了,先遍历数组,再遍历链表元素。
HashMap有两个参数会影响其性能,初始容量和加载因子,
31.说说你对hash算法的理解
是把任意长度的输入(又叫做预映射pre-image)通过hash算法变换成固定长度的输出,该输出就是散列值。

追问:hash算法任意长度的输入 转化为了 固定长度的输出,会不会有问题呢?
会产生hash冲突的问题,在程序中两个值经过hash算法之后得到同样的hash值就会产生hash冲突
追问:hash冲突能避免么?
理论上是不可避免的,举个例子吧,抽屉原理,9个抽屉 10个苹果,最终肯定会有一个抽屉的苹果数量大于一的。所以hash冲突无法避免,只能尽量避免。
2. HashMap 为什么要使用 hash 函数
Java 的 HashMap 中使用的是数组 + 链表的结构,但在保存时,一个 K - V 键值对应该被存放到数组的哪个位置?
通常我们都会想到:按照存入顺序存放。但是,按照这种策略,在取值时势必需要遍历整个数组,然后一个个去比较它们的 key 是否相等,这对于性能的损耗无疑是很大的。也许你已经猜到了,解决这个问题的办法就是散列函数。
3.HashMap中存储数据的结构是什么样的呢?
jdk8版本,hashmap是 数组+链表+红黑树构成的,每个数据单元都是Node结构,node结构中有key value next hash字段,next字段是在产生hash冲突的时候当前桶中的node 与冲突的node连成一个链表用的字段

4.创建HashMap时,不指定散列表数组长度,初始长度是多少呢?
散列表数组的长度默认初始值16
追问:散列表是new HashMap() 时创建的么?
不是,散列表是懒加载机制,只有在第一次put的时候才创建

5.默认负载因子是多少呢,并且这个负载因子有什么作用?
默认的负载因子是0.75,负载因子是计算扩容阈值用的,比如创建的hashmap对象,默认情况下扩容阈值是16 *0.75也就是12,,扩容原数组的两倍
加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,提供了查找效率
为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的
6.链表转化为红黑树,需要达到什么条件呢?
链表转红黑树主要是有两个指标,其中一个就是链表的长度达到8,另一个是当前散列数组的长度>=64,,否则链表的长度达到8,也不会链转树,只会发生一扩容

7.Node对象内部的hash字段,这个hash值是key对象的hashcode()返回值么??Jdk1.7的时候是取余运算,1.8是位运算
这个不是的,这个是经过二次计算得到的
追问:这个hash值是怎么得到呢?
key的hashcode值经过二次加工得到的,这个hash值是散列数组的地址,咱们都知道,散列数组的长度必须是2的次方,二次计算hash值的算法是,hash值高低位异或后 对散列数组长度进行取余,但是取余操作效率太低,不如计算机指令效率高,所以代码中 hash&(table.length-1) 来进行按位与计算,所以在散列数组是 二的次方的时候 hash&(table.length-1)==hash%table.length
table.length 例如默认值16 转换为二进制是10000 32转换为二进制是100000,是有规律的,2的次方装换为2进制都是高位是1 低位全是0的,而这种2的次方数减一的二进制都变成了111…,所以在进行按位与&,按位与& 运算,两个当且仅当都为1的时候结果才为1,
追问:hash字段为什么采用高低位异或?
主要是优化hash算法,如代码 (h = key.hashCode()) ^ (h >>> 16),当数组的长度很短时,只有低位数的hashcode值能参与运算(按位与运算)。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。

9.红黑树的写入操作,是怎么找到父节点的,找父节点流程?
10.TreeNode数据结构,简单说下。
treeNode结构继承了node结构加了几个字段,分别是指向父节点的parent,左节点的left,右节点的right,还有表示上一个节点的prev,还有判断颜色的red字段。这就是treeNode的基本结构

为什么要扩容呢,之所以要扩容,我理解是基于两个逻辑,

  1. HashMap中元素个数实在太多了,已经很频繁的发生hash冲突,我不得不进行扩容来减少这个冲突,来增强效率。
  2. HashMap中元素并不多,但元素都集中到那么一两个“坑位”上,数组占用的位置很少,但是在那一两个位置上的元素已经很多了,拖掉了执行的效率。即核心是一个分布不均匀的问题,在这样的情况下,扩容的目的,就是通过重新计算hash“坑位”让原来聚集在一起的节点分散开来,是为了让分布更加均匀。

为什么不直接将key作为哈希值而是与高16位做异或运算?
因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16为做异或运算使得在做&运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。
为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?
1.为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

追问:为什么采用位移运算,不是直接*2?
主要是因为性能,因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/949083.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号