HashMap实现原理

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


一、为什么需要散列表

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

  • 数组数组删除、插入性能不佳,寻址性能极优
  • 链表链表查询性能不佳,删除、插入性能极优

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


二、散列表原理

  • 散列表:散列表是一个根据key来访问value的存储结构,HashMap中实现的散列表是一个链表类型的数组,即数组+链表,用来存储key-value数据对。Ps:在1.8及以后版本的jdk中,HashMap中还加入了红黑树,关于这点后文会细说。

哈希函数与下标的计算

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

key-index映射过程

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

  • hash(key):对key进行hash计算,获得一个int类型的hash值 效果参考Object.hashCode()
  • index(hash, N+1):对上面得到的hash值进行计算,获得一个不超过数组大小的下标 效果参考 hash%(N+1)

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

哈希碰撞与下标冲突

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

  • 哈希碰撞的定义:有a,b两条数据,且a != b,对于这组数据,如果有hash(a) == hash(b),则哈希发生碰撞
  • 原因:hash函数的返回值是一个int类型的数据,int的取值是有范围的,而散列表的key是没有范围的,可以是任何值。将多数key映射到少数hashCode,必然会有多个key对应同一个hashCode的情况。
  • 可能性:许多人认为int的取值范围够大,在实际使用中很少会发生碰撞。其实不然,可以参见生日悖论,简单解释一下生日悖论:23人里有两人同一天生日的概率超过50%。(更加具体的说明请自行搜索“生日悖论”)同理,哈希碰撞的可能性是很大的。
哈希碰撞示意图

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

  • 下标冲突:与哈希的碰撞类似,在将hashCode映射到数组下标时也是有可能重复的。在往数组的某个下标插入节点的时候发现该下标已经有其他节点,即为下标冲突。
下标冲突示意图

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

如何解决碰撞和冲突

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

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

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

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

  • 举个例子,现在假设有节点B要添加到表中,那么遇到冲突后的整个流程是这样的:
    1、计算得到B.key对应的下标为 n
    2、判断 n 下标下是否有值
    3、发现有值,下标冲突,获取到该下标已有的节点A
    4、判断(B.key).equals(A.key)
    5、如果key相等,根据key唯一的特性,B节点覆盖A节点
    6、如果key不等,判断A.next是否有值
    7、如果A.next也有值,回到步骤4对A.next节点进行对比
    8、重复步骤4-7,直到发现key相等的节点,或遍历到链表的最后一个节点X,执行操作X.next = B
    Ps:读取节点的流程与添加类似,先计算下标,然后遍历链表,直到找到对应的key

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

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


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

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

扩容与负载因子

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

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

  • 默认容量:HashMap中数组的长度如果不指定,则默认为16
  • 2的n次幂:HashMap在new的时候可以指定数组长度,但不管如何指定,实际长度一定是2的n次幂(16、32、64、128等),举几个例子,指定长度为17或29,实际长度为32,指定为35、57或63,实际长度为64,以此类推。
  • 扩容:随着散列表存储节点的不断增加,散列表中数组的长度也应该增加,为了保证2的n次幂特性,每次扩容都是当前长度*2, 扩容的方法是,新建一个两倍的数组,然后遍历散列表中的所有节点,重新计算下标放入新数组。
  • 负载因子:由于散列存储下标具有不确定性,在数组即将被占满的时候,后续添加会发生大量冲突,为了避免,需要使数组在即将被占满前就扩容,而不是等待数组被占满。负载因子决定具体何时扩容。其默认值是0.75,可以在调用构造器的时候指定。0.75的意思是,在调用put方法时,算上被put的节点,如果当前数组被占用达到75%则进行扩容。

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

引入红黑树

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

  • 红黑树:红黑树是一个相对平衡的二叉查找树。平衡二叉树的查找原理和二分查找类似,时间复杂度也一样,都是从有序序列的中间开始查找。此处不做更详细的解释,只需要知道是一个查询效率很高的数据结构即可。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339