微信搜索【程序员囧辉】,关注这个坚持分享技术干货的程序员。
我的最新文章:面试官:如何进行 JVM 调优(附真实案例)
目录
二狗:先来点简单的,介绍下 HashMap 的底层数据结构吧。
二狗:(呦呦呦,时间和空间上权衡的结果,还装起B来了)那为什么转回链表节点是用的6而不是复用8?
二狗:(小菜鸡,懂得还不少)那 HashMap 有哪些重要属性?分别用于做什么的?
二狗:threshold 除了用于存放扩容阈值还有其他作用吗?
二狗:HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?
二狗:(你他娘的是在绕口令吧)你这个*@%¥#&的N次方是怎么算的?
二狗:(这叼毛讲的还凑合啊,连我都听懂了)你说 HashMap 的容量必须是 2 的 N 次方,这是为什么?
二狗:你说 HashMap 的默认初始容量是 16,为什么是16而不是其他的?
二狗:那我们换个问题问吧,HashMap 的插入流程是怎么样的?
二狗:图里刚开始有个计算 key 的 hash 值,是怎么设计的?
二狗:红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?
二狗:(尼玛,没听懂,尴尬了)那总结下 JDK 1.8 主要进行了哪些优化?
二狗:除了 HashMap,还用过哪些 Map,在使用时怎么选择?
二狗:(不妙,这个 B HashMap 懂得比我还多,得赶紧溜)到时间和女朋友吃饭了,我们之后再分胜负。
前言
某日,囧辉和同事二狗决定就谁是“&#¥*大厦11楼11室(只有囧辉和二狗两人)HashMap 最强者”展开一番较量。画面过于血腥,成年人请在未成年人陪同下观看。
正文
二狗:天天听你憨逼吹牛,是时候让你知道什么叫残忍了。
囧辉:二狗子,这屎可以乱吃,这话不能乱说哦。
二狗:先来点简单的,介绍下 HashMap 的底层数据结构吧。
囧辉:我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。
二狗:为什么要改成“数组+链表+红黑树”?
囧辉:主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。
二狗:那在什么时候用链表?什么时候用红黑树?
囧辉:对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。
二狗:为什么链表转红黑树的阈值是8?
囧辉:我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果(这 B 我装定了)。
红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。
理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006(跟大乐透一等奖差不多,中大乐透?不存在的),这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。
二狗:(呦呦呦,时间和空间上权衡的结果,还装起B来了)那为什么转回链表节点是用的6而不是复用8?
囧辉:如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。
二狗:(小菜鸡,懂得还不少)那 HashMap 有哪些重要属性?分别用于做什么的?
囧辉:除了用来存储我们的节点 table 数组外,HashMap 还有以下几个重要属性:1)size:HashMap 已经存储的节点个数;2)threshold:扩容阈值,当 HashMap 的个数达到该值,触发扩容。3)loadFactor:负载因子,扩容阈值 = 容量 * 负载因子。
二狗:threshold 除了用于存放扩容阈值还有其他作用吗?
囧辉:在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。
二狗:HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?
囧辉:默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。
二狗:(你他娘的是在绕口令吧)你这个*@%¥#&的N次方是怎么算的?
囧辉:Talk is cheap. Show you the code。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
二狗:卧槽,还彪英文,来来来,这代码你给我解释下。
囧辉:我们先不看第一行“int n = cap - 1”,先看下面的5行计算。
|=(或等于):这个符号比较少见,但是“+=”应该都见过,看到这你应该明白了。例如:a |= b ,可以转成:a = a | b。