如果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每次扩容后,容量变为原来的2倍。扩容操作主要发生在put操作中。
键值对个数超过当前容量*负载因子(假如当前容量为16,16*0.75=12)时,触发一次扩容
当链表长度超过
8,但容量没达到64时,只触发一次扩容,不触发树化
当容量达到64后,链表长度再+1时,才触发树化。
网上有很多分析,这里整理几条自己觉得比较合理的:
将单向链表转化为红黑树。树化操作主要发生在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;
}
上面四个位置只要有一个为空,就会解除红黑树。
理想情况下随机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 链表与红黑树转换