前言
今天给大家科普一个冷知识,毕竟这个问题的答案知道了对日常编码也没啥用,也只能用来坑人了(手动狗头)。
起因是这样的
HashMap里链表的最大长度到底是多少
其实很久以前在学习HashMap源码的时候有一直有这个疑问。看了网上很多篇文章都说的是链表长度达到8就会转成红黑树:传动门,看完源码总感觉哪里不太对,最近花了点时间仔细研究了下发现链表的最大长度并不是8。
话不多说,看源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//空表,执行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//size为2的幂次方的原因
if ((p = tab[i = (n - 1) & hash]) == null)
//空节点,直接插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//树节点执行插入树的操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//覆盖已经存在的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空方法,留给linkedHashMap实现
afterNodeAccess(e);
return oldValue;
}
}
//modCount用于记录操作的次数,迭代的时候会判断该值是否改变(快速失败)
++modCount;
//数组长度超过阈值,扩容
if (++size > threshold)
resize();
//空方法,留给linkedHashMap实现
afterNodeInsertion(evict);
return null;
}
我们这里只分析链表转红黑树的过程,就只截取最重要的一段代码了:
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// binCount >= 7
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
根据这段代码我画一个简图分析一下
可以看出来链表里此时有8个元素,当binCount=7的时候,判断(e = p.next) == null,就会进入if判断,添加一个新的节点到尾端,此时链表的长度是9,然后就会触发if (binCount >= TREEIFY_THRESHOLD - 1)语句,开始转红黑树。
所以很明显,链表是可以一直维持在8这个长度的,应该说是极限最大长度为9才行。
你以为这就完了吗
看明白这个过程的我忽视了一个非常重要的问题,当我开始debug观察map的时候发现了这样的现象:
(先上代码)
@AllArgsConstructor
public class DemoKey {
private Integer value;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DemoKey key = (DemoKey) o;
return Objects.equals(value, key.value);
}
@Override
public int hashCode() {
//确保每个key的hash相同,在put操作时会保证100%的hash冲突,全部放在一个桶里
return 0;
}
}
public class DemoHashMap {
public static void main(String[] args) {
Map<DemoKey, Integer> map = new HashMap<>();
for (int i = 1; i < 16; i++) {
//我在这里打了一个i==9的断点
map.put(new DemoKey(i), i);
}
}
}
可以看到到目前为止和我想的一模一样,链表长度已经为8,进入if判断,尾结点指向新建的第九个节点,然后触发treeifyBin方法,直到我看到这样的一幕。
此时size为9,但是还是进入了这个else判断中,说明上面的p instanceof TreeNode为false,明明触发了treeifyBin方法,为什么此时还是链表,而且长度保持在了9,经过测试,最大长度保持在了10,当第11个节点插入后成功转为了红黑树,并不是我之前设想的8。
相信说到这,大家都知道了我之前说的忽视了一个重要的问题指的是什么了吧,这也是一个非常经典的面试问题:HashMap链表转红黑树条件是什么,我们看一下treeifyBin方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//MIN_TREEIFY_CAPACITY=64, 代表形成红黑树最小的表长度
//如果tab为空,或者此时表的长度小于64时,不会触发链表转红黑树,而是进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
...
}
}
Demo里面我初始化map没有给初始化长度,默认长度为16,导致前两次触发treeifyBin方法时都是直接扩的容,这就导致链表的最大长度本来应该是8,结果两次转树失败,变成了10,当第11个节点进来时,数组长度为64,成功转为红黑树。
结论
1、HashMap链表转红黑树条件:
-
添加节点后,链表的长度大于8(等于8不会触发)
-
map的容量要大于等于64
2、链表的最大的稳定长度为8(超过就会扩容或者转红黑树了)
3、链表的理论最大长度为10(实际中hash碰撞不会这么剧烈,所以是理论最大值)
4、面试的时候不要问人家第3点,问了也不要说是我说的,年轻人还是要讲点武德比较好