讨论一下HashMap链表最大长度问题

717 阅读3分钟

前言

今天给大家科普一个冷知识,毕竟这个问题的答案知道了对日常编码也没啥用,也只能用来坑人了(手动狗头)。

起因是这样的

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;
                }

根据这段代码我画一个简图分析一下

image-20210319163917107

可以看出来链表里此时有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);
        }
    }
}

image-20210319165117500

可以看到到目前为止和我想的一模一样,链表长度已经为8,进入if判断,尾结点指向新建的第九个节点,然后触发treeifyBin方法,直到我看到这样的一幕。

image-20210319165456027

此时size为9,但是还是进入了这个else判断中,说明上面的p instanceof TreeNodefalse,明明触发了treeifyBin方法,为什么此时还是链表,而且长度保持在了9,经过测试,最大长度保持在了10,当第11个节点插入后成功转为了红黑树,并不是我之前设想的8。

image-20210319165744327

相信说到这,大家都知道了我之前说的忽视了一个重要的问题指的是什么了吧,这也是一个非常经典的面试问题: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点,问了也不要说是我说的,年轻人还是要讲点武德比较好