HashMap扩容、树化分析

1 篇文章 0 订阅


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 链表与红黑树转换

HashMap树化部分源码如下: ```java final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } } ``` 这部分代码主要是将一个桶中的链表转化为红黑树。具体步骤如下: 1. 如果数组为空或者长度小于MIN_TREEIFY_CAPACITY,调用resize()函数扩容数组; 2. 如果桶中的第一个元素不为空,将链表转化为红黑树; 3. 将链表中的每个节点都替换为TreeNode类型的节点,并将它们链接成双向链表; 4. 将双向链表转化为红黑树。 其中第3步中的replacementTreeNode()方法代码如下: ```java TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } ``` 该方法用于将普通节点转换为红黑树节点。最后一步中的treeify()方法代码如下: ```java final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); } ``` 该方法用于将双向链表转换为红黑树。具体步骤如下: 1. 创建一个root节点,将第一个节点作为root节点; 2. 遍历双向链表中的每个节点,将它们插入到红黑树中; 3. 在插入节点时,根据节点的hash值比较大小,将节点插入到左子树或右子树中; 4. 插入节点后,将红黑树进行平衡,保证红黑树的性质; 5. 将root节点移到数组的最前面,保证root节点在桶的第一个位置上。 总之,HashMap树化部分主要是将链表转化为红黑树,以提高查询效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值