jdk1.8 HashMap链表转红黑树从put到treeify
Posted zhangjin1120
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdk1.8 HashMap链表转红黑树从put到treeify相关的知识,希望对你有一定的参考价值。
什么情况下转红黑树?
下面两个条件必须同时满足,才会转红黑树
- 当前插入链表的长度大于或等于8。
- HashMap中的数组,长度大于或等于64。
如果只是链表长度大于等于8,数组长度没有达到64,只会扩容,不会转红黑树。
从put方法到红黑树插入的具体过程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这个tab马上会被赋值为成员变量里的table
HashMap.Node<K,V>[] tab;
HashMap.Node<K,V> p;
int n, i;
// 如果tab数组未被初始化,则初始化该数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// hash值对应的tab[i]为空,直接放到tab数组中,作为第一个元素
if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
}else {
//新的链表结点
HashMap.Node<K,V> e;
K k;
//如果hash值恰好与对应桶的首个对象p一样,那么不用考虑,直接替换
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果Node的类型是红黑树,将值更新到树中
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 依次遍历链表,binCount主要用来判断什么时候由链表转换成红黑树
for (int binCount = 0; ; ++binCount) {
// 如果遍历到链表末端,还没有插到链表中
if ((e = p.next) == null) {
//生成一个新的节点,放到链表的末尾,newNode方法并没有做什么
//只是把hash,key,value整合到一个新的Node中。
p.next = newNode(hash, key, value, null);
// 链表长度大于等于8(注意binCount的开始值是0,如果是1,
//就是(binCount>=TREEIFY_THRESHOLD)
//treeifyBin()内部不一定转红黑树,还要看数组长度是否到了64
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// e就是p.next,那么e.hash如果与要put的key碰撞到,那么直接退出循环,此处的逻辑判断与桶首个对象的判断逻辑一样
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果没找到相同的key和hash 那么将e赋值为当前p,让他到下次循环中
p = e;
}
}
// e!=null 代表原来Map中存在这个key
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 如果onlyIfAbsent=false,或者old_Value=null,不产生新的对象,直接替换value,默认是直接替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//将这个元素放到链表尾部 1.7之前是头插
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果桶的数量超过阈值
if (++size > threshold)
//重新分布桶,如果多线程的时候 会出现循环链表的情况,造成CPU升高,值错乱
resize();
afterNodeInsertion(evict);
return null;
}
//上面用到的newNode,并没有做啥。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
/**
* tab:元素数组,
* hash:hash值(要增加的键值对的key的hash值)
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
/*
* 如果元素数组为空 或者 达不到成树的条件
* MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换。
*/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 扩容
// 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
// 根据hash值和数组长度进行取模运算后,得到链表的首节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null; //首(head)尾(tail)节点
do {
TreeNode<K,V> p = replacementTreeNode(e, null); //根据Node新建一个TreeNode
if (tl == null) //hd只是保存新生成的双向链表
hd = p; // 这行代码在循环中只运行一次。
else {
p.prev = tl; //新结点的前驱赋值为tl
tl.next = p; // 尾节点的 后继指向 新节点
}
tl = p; // 尾结点向后移动
} while ((e = e.next) != null); // 继续从头到尾遍历单链表
// 到目前为止 也只是把Node对象转换成了TreeNode对象,
//把Node单向链表转换成了TreeNode双向链表
// 转换后的双向链表,替代原来位置上的单向链表
if ((tab[index] = hd) != null)
hd.treeify(tab);//双链表转红黑树
}
}
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
//接着看treeify做了什么
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null; // 定义树的根节点
// 遍历链表
// 调用treeify的代码是:hd.treeify(tab); 这个this,就是之前的hd
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next; // 给next赋值
x.left = x.right = null; // 设置当前节点的左右节点为空
if (root == null) { // 这个只运行一次
x.parent = null; // 当前节点的父节点设为空
x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
root = x; // 根节点指向到当前节点
}
else { // 根节点赋值完成后
K k = x.key; // 取得当前链表节点的key
int h = x.hash; // 取得当前链表节点的hash值
Class<?> kc = null; // 定义key所属的Class
for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出
// GOTO1
int dir, ph; // dir 标识方向direction(左右)、ph标识当前树节点的hash值
K pk = p.key; // 当前树节点的key
if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
dir = -1; // 标识当前链表节点会放到当前树节点的左侧
else if (ph < h)
dir = 1; // 右侧
/*
* 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
* 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
* 如果还是相等,最后再通过tieBreakOrder比较一次
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // 保存当前树节点
/*
* 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
* 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
* 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
* 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
* 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
*/
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;
}
}
}
}
// 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
// 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
moveRootToFront(tab, root); // 单独解析
}
总结一下:
put操作中,如果入参结点的key不存在,则通过尾插法,将入参结点插入到单链表的末尾。然后判断链表长度是否达到8,如果达到8,并且HashMap中数组长度已经达到64,则会将入参结点所在的链表,转为TreeNode双向链表,然后再将TreeNode双向链表转为红黑树。
先分析到这里,后续再分析,双链表到底是怎么转为红黑树的。
以上是关于jdk1.8 HashMap链表转红黑树从put到treeify的主要内容,如果未能解决你的问题,请参考以下文章
ConcurrentHashMap在jdk1.8中的改进-时间复杂度从O(n)到O(log(n))-链表转红黑树的值是8