在极限状态下,HashMap放多少数组会变成红黑二叉树?

本文深入探讨HashMap在何种条件下会从链表转换为红黑树。通过代码实验验证,揭示了HashMap在极限状态下面对扩容机制时,如何判断是否进行结构转换的细节。

在极限状态下,HashMap放多少数组会变成红黑二叉树?

回答问起前,我们先回顾下HashMap
HashMap底层是数组+链表的方式
HashMap具有唯一性
每个链条默认情况下在放入第九个元素后,该链条就会自动从Node变成TreeNode
但情况真的是这样吗?

我们可以使用java来进行验证

public class TextMap {

    public static void main(String[] args){

        HashMap<User, User> map = new HashMap<User, User>();

        for(int i = 1; i <= 20; i++){
            User user = new User();

            map.put(user , user);

            System.out.println("i =" + i);
        }
    }
}

class user{

    //保证每个元素放入同一个索引,将hashcode设置为1
    @Override
    public int hashCode() {
        return 1;
    }

    //保证在同一个索引中,每个元素不会因为相同被覆盖,将equles设置为false
    @Override
    public boolean equals(Object obj) {
        return false;
    }
}

结果:
当放入到第十个元素时,此时的HashMap还是没有转为红黑二叉树
在这里插入图片描述
但是当放入第十一个元素的时候,此时的Node转为了TreeNode
在这里插入图片描述
因此我们得出结论:
在极限状态下,HashMap在同一索引中放入第十一个元素时才会转变为红黑二叉树

那么由此我们会想为什么会这样呢?Java中明明预设了阈值"8",怎么会到第十一才转为红黑二叉树呢?

其实在HashMap中存在扩容机制,
它一开始默认是长度为16的数组,
当某一个索引中的元素超过到8时,它优先会选择将长度扩容2倍(即长度为32)的数组,
它会认为是不是由于数组的长度不够才导致一个索引中元素过多;
但当它发现长度为32时,某一个索引中的元素还是超过9时,
它还是会优先选择将长度再次扩容到2倍(即长度为64)的数组,
在长度为64的数组中,它发现某一个索引中的元素还是超过10时,它就会对该索引所在的链表转化为红黑二叉树.
所以在第十一个元素时,就会对该索引所在的链表转化为红黑二叉树.

### HashMap扩容条件 HashMap 中的数扩容主要由以下两个条件触发: 1. **元素数量超过阈值(threshold)**:当 HashMap 中存储的键值对数量超过当前数容量与负载因子(load factor)的乘积时,会触发扩容操作。默认情况下,初始容量为 16,负载因子为 0.75,因此默认的扩容阈值为 `16 * 0.75 = 12`。当元素数量超过 12 时,数将进行扩容[^2]。 2. **链表长度过长**:在 JDK 1.8 中,如果某个桶(bucket)中的链表长度超过阈值(默认为 8),并且当前数容量大于等于 64,会将该链表转换为红黑树以提高查找效率。如果数容量小于 64,则优先进行扩容而不是转换为红黑树[^4]。 ### HashMap扩容机制 HashMap 的扩容机制主要涉及以下几个步骤: 1. **创建新数**:在扩容时,HashMap 会创建一个新的数,其容量是原数的两倍。例如,原数容量为 16,扩容后变为 32。新数的阈值也会根据新的容量和负载因子重新计算[^1]。 2. **重新计算哈希值**:在将元素迁移到新数时,需要重新计算每个键的哈希值,以确定其在新数中的索引位置。在 JDK 1.7 和 JDK 1.8 中,重新计算索引的方式略有不同: - 在 JDK 1.7 中,直接对键的哈希值进行取模运算来计算索引。 - 在 JDK 1.8 中,由于数容量始终是 2 的幂次,因此可以通过位运算(`hash & (newCap - 1)`)来计算索引,这比取模运算更高效[^4]。 3. **迁移元素**:在重新计算索引后,将原数中的元素迁移到新数中。对于每个桶中的链表或红黑树,需要逐个迁移节点。在 JDK 1.8 中,为了提高迁移效率,引入了“高低位迁移”的概念。具体来说,每个键的哈希值在扩容后的新索引位置只与哈希值的一个高位有关。因此,可以将链表分为两个部分:高位为 0 的节点保留在原索引位置,高位为 1 的节点迁移到原索引加原容量的位置。这种方式避免了重新计算哈希值,提高了迁移效率[^4]。 4. **更新哈希表**:迁移完成后,新的数将取代原数。同时,更新 HashMap 的内部变量(如 `table`、`threshold` 等)以反映新的数状态[^3]。 以下是一个简化的扩容代码示例: ```java void resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 计算新容量和新阈值 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 更新阈值 threshold = newThr; // 创建新数 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 迁移元素 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } } ``` ###
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值