exam

HashMap实现原理

2019-03-28  本文已影响0人  monkey丶咕噜

HashMap在实际开发中用到的频率非常高,面试中也是热点。所以决定写一篇文章进行分析,希望对想看源码的人起到一些帮助,看之前需要对链表比较熟悉。
以下都是我自己的理解,欢迎讨论,写的不好轻喷。


一、为什么需要散列表

HashMap中的数据结构为散列表,又名哈希表。在这里我会对散列表进行一个简单的介绍,在此之前我们需要先回顾一下 数组链表 的优缺点。

数组的优缺点取决于他们在内存中存储的模式,也就是直接使用顺序存储链式存储导致的。无论是数组还是链表,都有明显的缺点。而在实际业务中,我们想要的往往是寻址、删除、插入性能都很好的数据结构,散列表就是这样一种结构,它巧妙的结合了数组与链表的优点,并将其缺点弱化(并不是完全消除)


二、散列表原理

哈希函数与下标的计算

散列表的做法是将key映射到数组的某个下标,存取的时候通过key获取到下标(index)然后通过下标直接存取。速度极快,而将key映射到下标需要使用散列函数,又名哈希函数。说到哈希函数可能有人已经想到了,如何将key映射到数组的下标。

key-index映射过程

图中计算下标使用到了以下两个函数:

值得注意的是,下标并不是通过hash函数直接得到的,计算下标还要对hash值做index()处理。
Ps:在散列表中,数组的格子叫做,下标叫做桶号,桶可以包含一个key-value对,为了方便理解,后文不会使用这两个名词。

哈希碰撞与下标冲突

以下是哈希碰撞相关的说明:

哈希碰撞示意图

以下是下标冲突相关的说明:

下标冲突示意图

很多人认为哈希值的碰撞和下标冲突是同一个东西,其实不是的,它们的正确关系是这样的,hashCode发生碰撞,则下标一定冲突;而下标冲突,hashCode并不一定碰撞

如何解决碰撞和冲突

上文提到,在jdk1.8以前HashMap的实现是散列表 = 数组 + 链表 ,但是到目前为止我们还没有看到链表起到的作用。事实上,HashMap引入链表的用意就是解决下标冲突。

下图是引入链表后的散列表:

图片来源于百度百科-哈希表词条

如上图所示,左边的竖条,是一个大小为16的数组,其中存储的是链表的头结点,我们知道,拥有链表的头结点即可访问整个链表,所以认为这个数组中的每个下标都存储着一个链表。其具体做法是,如果发现下标冲突,则后插入的节点以链表的形式追加到前一个节点的后面

这种使用链表解决冲突的方法叫做:拉链法(又叫链地址法)。HashMap使用的就是拉链法,拉链法是冲突发生以后的解决方案。

Q:有了拉链法,就不用担心发生冲突吗?
A:并不是!由于冲突的节点会不停的在链表上追加,大量的冲突会导致单个链表过长,使查询性能降低。所以一个好的散列表的实现应该从源头上减少冲突发生的可能性,冲突发生的概率和哈希函数返回值的均匀程度有直接关系,得到的哈希值越均匀,冲突发生的可能性越小。为了使哈希值更均匀,HashMap内部单独实现了hash()方法。


三、HashMap中散列表的实际运用

以上是散列表的存储结构,但是在被运用到HashMap中时还有其他需要注意的地方,这里会详细说明。

扩容与负载因子

现在我们清楚了散列表的存储结构,细心的人应该已经发现了一个问题:Java中数组的长度是固定的,无论哈希函数是否均匀,随着插入到散列表中数据的增多,在数组长度不变的情况下,链表的长度会不断增加。这会导致链表查询性能不佳的缺点出现在散列表上,从而使散列表失去原本的意义。为了解决这个问题,HashMap引入了扩容与负载因子。

以下是和扩容相关的一些概念和解释:

Ps:扩容要重新计算下标扩容要重新计算下标扩容要重新计算下标,因为下标的计算和数组长度有关,长度改变,下标也应当重新计算。

引入红黑树

在1.8及其以上的jdk版本中,HashMap又引入了红黑树。

红黑树的引入被用于替换链表,上文说到,如果冲突过多,会导致链表过长,降低查询性能,均匀的hash函数能有效的缓解冲突过多,但是并不能完全避免。所以HashMap加入了另一种解决方案,在往链表后追加节点时,如果发现链表长度达到8,就会将链表转为红黑树,以此提升查询的性能。

上一篇 下一篇

猜你喜欢

热点阅读