HashMap扩容、树化分析


HashMap结构

IDEA查看集合结构

如果IDEA的debug调试模式断点看不到HashMap中的数组属性,可以这么操设置:
settings ==> Build,Execution.Deployment ==> Debugger ==> Data Views ==> Java ==> Enable alternative view for Collections classes(去掉勾选)
在这里插入图片描述
另外,Hide null elements in arrays and collections(去掉勾选)可展示空元素。

HashMap一些属性

  • DEFAULT_INITIAL_CAPACITY:默认初始容量,值为16
  • DEFAULT_LOAD_FACTOR:负载因子,值为0.75
  • MAXIMUM_CAPACITY:最大容量,值为2^30
  • TREEIFY_THRESHOLD:链表转换为红黑树的链表长度阈值,值为8
  • UNTREEIFY_THRESHOLD:红黑树转换为链表的链表长度阈值,值为6
  • MIN_TREEIFY_CAPACITY:链表转换为红黑树的容量阈值,值为64

扩容

HashMap每次扩容后,容量变为原来的2倍。扩容操作主要发生在put操作中。

触发扩容的条件

  • 键值对个数超过当前容量*负载因子(假如当前容量为16,16*0.75=12)时,触发一次扩容

  • 当链表长度超过8,但容量没达到64时,只触发一次扩容,不触发树化

    当容量达到64后,链表长度再+1时,才触发树化。

HashMap为何没有缩容

网上有很多分析,这里整理几条自己觉得比较合理的:

  • 如果要缩容,肯定是在remove方法中操作,这会导致时间复杂度从O(1)变成O(n),这是不可接受的(Java在大部分情况下都是用空间换时间的,缩容却要用时间换空间)
  • remove的时候,会将Node实体的指针已经置为null,GC会释放实体,所以缩容缩的只是那个已经分配的数组,意义不大(最占空间的是那个Node实体,而不是已分配的数组空位置)

树化

将单向链表转化为红黑树。树化操作主要发生在put操作中。

触发树化的条件

  • 当链表长度超过8,且容量达到64时,触发树化

红黑树重新转为链表

将红黑树转化为单向链表主要发生在remove操作和resize(扩容)操作中。

  • 在resize操作中,如果节点为TreeNode(红黑树),会执行TreeNode的split方法分割红黑树。

    split方法会先根据hash取模的值将红黑树分割为两个红黑树,然后判断这两个新的红黑树长度如果小于等于6,会将红黑树转化为单向链表。

  • 在remove操作中,进入到removeNode方法,判断要删除的节点是否为TreeNode,如果是则进入删除红黑树节点的removeTreeNode方法,方法中判断是否要解除红黑树的条件为:根节点为空或者根节点的右节点为空或根节点的左节点为空或根节点的左节点的做左节点为空。
    请添加图片描述

    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);
        return;
    }
    

    上面四个位置只要有一个为空,就会解除红黑树。

HashMap为何不直接使用红黑树

  • 时间维度:当链表长度很小时,即使遍历,查询速度也非常快O(n);当链表不断变长,对查询性能有一定的影响,而转成树,能使查询性能提升到O(log(n))。
  • 空间维度:因为红黑树需要进行左旋,右旋操作,本来就是以空间换时间。所以红黑树的空间成本比链表更大。

理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,在负载因子0.75的情况下,链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,是根据概率统计决定的,是为了让树化的几率足够小。

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

综上所述:

  • 链表空间成本相比更低,但如果长度过程,时间成本会变高;红黑树空间成本更高,但可以降低时间成本;
  • 当数据量少的时候,两者觉得时间成本都差不多且都比较低。所以没必要使用红黑树(更高的空间成本);当数据量大的时候链表的时间成本明显高于红黑树,这时使用红黑树(用更高的空间成本来换取时间成本的降低)才有意义。

所以,HashMap选择先使用链表,只有达到一定长度后,时间成本变高才会使用红黑树,用更大的空间成本来换取时间成本的降低。

链表与红黑树转换阈值

为什么链表转换为红黑树的阈值是8,而红黑树重新转换为链表的阈值是6,而不是7之类的?

个人认为,阈值有间隔主要是为了避免因为频繁的插入和删除操作二导致链表和红黑树之间频繁的转换,影响效率。
HashMap的一下几点设计可以佐证:

  • HashMap根据阈值6,从红黑树转换为链表发生在扩容操作的时候,而不是删除操作的时候。因为扩容的频率更低
  • HashMap删除操作的时候,是否将红黑树转换为链表并不是根据阈值6来判断的(具体判断规则参数上文内容),这种判断降低了红黑树转换为链表的概率。



参考文章:HashMap 链表与红黑树转换

<think>好的,我现在需要帮助用户了解Java HashMap扩容过程中对红黑的处理机制。用户的问题涉及红黑扩容时的转化、链表分裂和处理机制。让我先回忆一下HashMap的相关知识,并结合提供的引用内容来整理答案。 首先,HashMapJava 8之后引入了红黑来优化哈希冲突时的查询性能。当链表长度超过8时,链表会转为红黑;而当节点数减少到6以下时,红黑会转回链表。这在扩容过程中可能会有影响,因为扩容会导致哈希表的桶数量增加,原来的元素需要重新分布到新的桶中。 接下来是扩容的过程。扩容时,HashMap会创建一个新的数组,并将旧数组中的元素重新计算位置后迁移到新数组。对于普通的链表节点,这个过程相对简单,只需要根据新的数组长度重新计算索引,并将节点分配到高位或低位链表中。但对于红黑节点,处理起来会更复杂。 根据引用[3]和[4],当红黑节点需要分裂时,会将其拆分为高位和低位两个链表。然后检查这两个链表的长度,如果长度小于等于6,就会将红黑转回链表;否则,如果长度仍然超过6,可能需要重新树化,但根据引用[4]中的说明,可能不会直接重新树化,而是先保持链表结构,直到再次满足树化条件。这里可能需要更仔细地查看源码逻辑。 引用[4]提到,当红黑节点在扩容时被拆分后,如果拆分后的链表长度小于等于6,会调用untreeify方法将节点转换为普通链表节点。这个方法遍历节点,将它们转换为普通的Node,并形成链表。而如果拆分后的链表长度仍然超过6,是否需要重新树化呢?根据我的记忆,在扩容过程中,拆分后的链表如果长度超过8,会在新的桶中重新树化。不过需要确认这一点是否正确,或者是否有其他条件。 此外,引用[3]提到,哈希函数和冲突处理中,红黑的使用是为了提高查询效率。扩容时,红黑节点需要重新分布,这可能涉及到拆分操作。根据源码分析,当处理红黑节点时,会遍历结构,将节点根据新的哈希值分配到高位或低位链表中,然后分别处理这两个链表是否需要转换为或链表。 总结一下,处理步骤大致如下: 1. 扩容时,每个桶中的节点(包括红黑)需要重新计算索引,分为高位和低位。 2. 对于红黑节点,拆分后的高位和低位链表需要检查长度。 3. 如果拆分后的链表长度≤6,则转换为普通链表;否则,检查是否满足重新树化的条件。 4. 转换过程中,可能需要将节点转换为链表节点,或者重新构建红黑。 需要注意的是,源码中的具体实现可能涉及更多的细节,比如红黑拆分方法,是否保留结构还是直接转换为链表。根据引用[4]中的untreeify方法,当需要转换时,会遍历节点并生成链表。这可能意味着在拆分后,无论长度如何,都会先转为链表,再根据长度决定是否树化。或者可能是在拆分时保留结构,但需要进一步确认。 另外,用户的问题中提到了分裂处理机制,可能涉及红黑扩容时的分裂方式。例如,红黑如何被拆分为两个链表,以及如何决定每个链表的位置。根据哈希值的计算,新的索引可能是在原位置(低位)或原位置加上旧容量(高位),这与普通链表节点的处理类似。因此,红黑节点在拆分时,会根据新的哈希值分布到高位或低位,然后分别处理每个部分的节点数量。 还需要确认,在拆分红黑时,是否有可能其中一个拆分后的链表仍然足够长,需要保持结构。例如,如果原红黑有10个节点,拆分后高位有7个,低位有3个,那么高位可能需要保持结构,而低位转为链表。不过根据阈值,拆分后的高位链表长度为7,仍然大于6,但树化阈值是8,所以这时候可能不会树化,而是保持链表结构,直到再次添加节点超过8才会树化。这里可能存在一个误区,需要仔细核对。 根据JavaHashMap实现,当链表长度超过8时才会转为红黑,而转为链表的阈值是6。因此,在扩容拆分后,如果某个链表的长度是7,它仍然是一个链表,只有当再次插入元素使其长度达到8时才会树化。因此,在扩容过程中,拆分后的链表即使长度为7,也不会立即树化,而是保持链表结构。 总结处理机制: - 扩容时,红黑节点被拆分为高位和低位链表。 - 每个链表长度若≤6,转为普通链表。 - 若长度>6,保持链表结构,直到后续操作可能触发再次树化。 需要确保回答中正确反映这一点,并引用提供的参考文献。例如,引用[4]中的untreeify方法用于将节点转为链表节点,而引用[3]提到了树化条件和冲突处理。 最后,用户可能需要了解整个流程的步骤,以及为何要这样设计(例如性能优化,避免频繁树化和链表转换)。因此,回答中应分步骤说明处理过程,并引用相关的源码或机制说明。</think>Java HashMap扩容时对红黑的处理机制包含以下关键步骤: --- ### **1. 红黑节点分裂** 扩容时,HashMap 会根据新容量重新计算键的哈希值,将原桶中的红黑节点分为**低位链表**(索引不变)和**高位链表**(索引 = 原索引 + 旧容量)[^3]。 例如,旧容量为 16 的桶中,哈希值为 `key.hash & 15`。扩容后容量为 32,哈希值为 `key.hash & 31`,高位链表的索引为原索引 + 16。 --- ### **2. 链表长度阈值判断** 拆分后的链表需要根据长度决定是否保留红黑结构: - **若链表长度 ≤ 6**:调用 `untreeify()` 方法将红黑节点转换为普通链表节点[^4]。 - **若链表长度 > 6**:保留链表结构,但不会立即重新树化。只有当后续操作(如插入节点)导致链表长度 ≥ 8 时,才会再次触发树化[^3]。 --- ### **3. 红黑转链表的具体实现** 通过 `untreeify()` 方法遍历节点,逐个转换为普通链表节点: ```java final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; for (Node<K,V> q = this; q != null; q = q.next) { Node<K,V> p = map.replacementNode(q, null); // 转换为普通节点 if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } ``` 此方法将红黑节点替换为 `Node` 类型,形成普通链表[^4]。 --- ### **4. 重新树化的条件** 拆分后的链表在后续操作中可能重新树化: - 插入新节点时,若链表长度 ≥ 8 **且当前数组容量 ≥ 64**,链表会转为红黑[^3]。 - 若数组容量 < 64,则优先触发扩容而非树化。 --- ### **设计目的** 1. **性能优化**:避免频繁的树化与链表化操作,减少计算开销。 2. **空间效率**:拆分后链表较短时,链表的遍历效率高于红黑[^4]。 3. **动态适应**:根据数据分布动态调整结构,保证增删查操作的高效性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值