Java- 技术专题 -JDK8-HashMap 的实现原理
一 HashMap底层存储结构
HashMap底层结构采用(数组)+(链表 or 红黑树)的形式来存储节点。
首先HashMap是一个数组,而且数组里面每个位置可以放入多个元素,形象一点,咱们把数组的这些个位置称为桶。HashMap里面每个元素通过key值取hash在 & (数组长度容量-1)就可以唯一确定该元素属于哪个桶。
HashMap为了最大限度的提高效率,桶可能是链表也可能是红黑树。开始桶里面元素不多的时候采用 链表形式保存。后续随着HashMap里面的元素越来越多,桶里面里面的元素元素也越来越多,当元素 个数>8(TREEIFY_THRESHOLD)并且数组的长度>64(MIN_TREEIFY_CAPACITY)的时候,会把 链表变成红黑树(树化);
如果桶当前采用的是红黑树保存节点元素信息随着节点个数的减少(HashMap remove操作),当节
点元素个数<6(UNTREEIFY_THRESHOLD)的时候,又会把红黑树降级为链表。
HashMap的桶有了链表为啥还要转换为红黑树?HashMap源码大神考虑到链表太长的话。节点元素的查找效率不高。所以有链表转红黑树,红黑树转链表的操作。可能你又会想为啥不用平衡二叉树来替换红黑树。那是因为HashMap源码大神兼顾了节点的插入删除效率和节点的查询效率。
红黑树不追求"完全平衡"。所以往红黑树里面插入或者删除节点的时候任何不平衡都会在三次旋转之内解决。而平衡二叉树插入或者删除节点的时候为了追求完全平衡,旋转的次数是不固定的,花费时间跟多。
二 HashMap源码分析
了解了HashMap的存储结构之后,咱们着重对HashMap添加节点的过程做一个简单的分析。我相信只要咱们搞懂了HashMap里面添加元素的过程。HashMap里面大部分的实现逻辑咱们都能搞懂。
因为HashMap中最关键的部分(扩容、树化)在HashMap添加元素过程中都很好的体现出来了。这里我们先给出HashMap添加元素的简单流程图(HashMap里面putVal()函数流程图)。
上面流程图里面有几个重要的地方是我们需要重点关注的:resize()扩容,treeifyBin()树化。下面我们对resize()扩容,treeifyBin()树化的具体逻辑做一个简单的分析。
2.1 准备工作
HashMap里面一些关键属性介绍
HashMap容量(数组大小,桶的个数)永远是2的整数次幂
时时刻刻要记住HashMap的容量(桶的个数)永远是2的整数次幂。初始容量16,每次扩容之后的容量都是前一次容量的两倍。比如当前容量是16扩容一次编程32,再扩容一次变成64。
而且如果我们在new HashMap的时候,给了初始容量,但是给定的容量不是2的整数次幂,构造函数内部也会调用tableSizeFor()函数转换成2的整数次幂的。比如:传递3会转换成4,传递13会转换成16。
HashMap里面的元素桶的位置是怎么确定的(HashMap元素在数组中的索引位置)
很简单通过对元素的key做hash处理在 &(容量-1),就精确找到找个这个元素应该放入哪个桶中。
2.2 resize()扩容
一定要时刻记住HashMap的容量(数组的大小,我们也说桶的个数)永远都是2的整数次幂,扩容就是把容量扩大一倍。扩容之后的容量=原来容量*2。(桶的个数翻倍)就算你new HashMap()的时候给定的容量不是2的整数次幂。HashMap内部也是会通过tableSizeFor()函数把容量转换成2的整数次幂。
HashMap确定元素属于哪个桶是通过对该元素的key取hash之后再 & (容量-1)。这样就找到了桶的位置(其实是数组的下标)。
扩容前后桶的关系也要特别注意,扩容前属于同一个桶(桶的索引位置相同)里面的元素,在扩容之后只会有两个桶来放他们:要不还保留扩容前的桶的索引位置,要不就是通过扩容前的索引位置+扩容前的容量得和值确定位置。我们举个例子,假设原来的容量是16,那么扩容之后的容量就是32。
假设原来桶的位置为index。那么这个桶里面的元素只会去到扩容之后桶的index位置,或者桶的index+16位置。为什么会有这样的规律,关键在容量永远都是2的整数次幂。而且扩容是*2的扩容。为了加深大家的理解,我用一个图例来说明。
有了上面的理解,接下来,我们看HashMap的扩容逻辑就简单了,我们就直接贴代码,加注释了。
2.3 treeifyBin()树化
HashMap的树化,就是把链表转换为红黑树。当往HashMap里面添加元素的时候,随着桶里面元素的增加,当桶里面元素的个数大于8(TREEIFY_THRESHOLD),并且HashMap的容量大于64(MIN_TREEIFY_CAPACITY)的时候才会把链表树化成红黑树。
先转换成二叉树,在对二叉树做红黑树的平衡旋转处理。关于红黑树的原理,建议大家去网上找一些资料看看,还是挺有意思的。
红黑树特性
左子节点小于右子节点。(方便搜索)
节点是红色或者黑色。
根节点是黑色。
每个叶子的节点都是黑色的空节点(NULL)。
每个红色节点的两个子节点都是黑色。(从每个叶子节点到根节点的所有路径上不能有两个连续的红色节点)
从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
三 总结
通过对HashMap的实现做简单的分析,咱们可以总结出如下信息:
HashMap的初始容量是16。
HashMap的容量永远都是2的整数次幂,扩容之后的容量 = 扩容之前的容量*2。
HashMap扩容时机,当HashMap里面的元素个数 > 容量 * loadFactor (默认0.75)。
HashMap树化时机(链表转红黑树),当桶里面的元素个数 >= 8(TREEIFY_THRESHOLD),并且HashMap的容量 > 64(MIN_TREEIFY_CAPACITY)。
HashMap去树化的时机(红黑树转链表),当红黑树里面的元素个数 <= 6(UNTREEIFY_THRESHOLD)。
HashMap是线程不安全的,我们在分析过程中没有看到任何线程安全的保障。
评论