java集合-HashMap1.8


前言

众所周知,jdk8的HashMap的链表会在一定长度转换为红黑树,且很多方法也与1.7有了很大变化。

一、红黑树

规则:

  • 根节点为黑;
  • 叶子结点为黑色【叶子结点指的是Null节点
  • 插入的节点默认为红色
  • 红色节点的子节点必为黑色;
  • 从其中某一个节点开始,到其最底层节点【NULL节点】的路径上的黑色节点数目都是相同的

保证红黑树良好的插入查找特性的最重要的一条规则时最后一条。

1.1 插入规则

总的概括起来来说只有三条规则,牢牢记住就好:
在这里插入图片描述
需要注意的是:一、三是等价的。
插入的调整是递归的,永远优先考虑插入节点的祖父子三代,调整成功之后再去调整上面的节点,因此往往需要parent指针,向上递归。


  • 若父节点为黑色,直接插入【因为默认插入的是红色,且红色会带来一个黑色的null节点,那么这条路径上的黑色节点数目是不变的】

  • 若父节点为红色,且叔叔节点为黑色节点或者空:【为null或者黑色是等价的,同样处理即可】
    此时优先选择变色:

  1. 将父节点变为黑色

因为父节点为红,祖父节点必不为红,因此调整叔叔节点即可。因为父节点变成黑色,此时这条路径就会多一个黑色,调整叔叔节点为黑,就可以解决祖父节点以下的问题,但是祖父节点以下的所有路径都会多一个黑色节点,需要另外做调整。

祖父节点若为根节点,就没有问题了:
在这里插入图片描述
父亲和叔叔变色,因为祖父为根节点,多两个黑色节点没有影响:
在这里插入图片描述

而假设祖父节点上面还有节点,乍一看叔叔为黑色怎么也不行的:
叔叔那边必然比父亲这边多一个黑色节点,无法达到平衡。
其实这种情况一般情况确实不会出现,在调整完成之后不可能出现,但是在调整过程中可能出现,如:父亲、叔叔都为红色的插入:
在这里插入图片描述
此时按照优先变色原则,父亲和叔叔都变成黑色,可以达到祖父以下这条路径的平衡。
在这里插入图片描述
但是对于祖父以上的节点,祖父以下的所有路径就会多出一个黑色节点,优先变色原则,将祖父变成红色:
在这里插入图片描述

此时,若是吧祖父当做新插入的节点,就会出现祖父的叔叔为黑色,父亲为红色的情况

此时,因为祖父的叔叔节点的路径已经不能调整,我们只能选择旋转:【分为左旋和右旋,其实都是一样的,按照左旋来想象:】《以下称呼都是对于祖父节点而言的》
将父节点作为根节点,将父节点的左孩子作为祖父节点的右孩子,将祖父节点作为父节点的左孩子,父节点的右孩子不变。
在这里插入图片描述
此时,进行旋转后的颜色调整:
在这里插入图片描述

二、HashMap关于红黑树方法

2.1 TreeNode内部方法

先看一下TreeNode的数据结构:

在这里插入图片描述
不仅拥有左右孩子,还有一个prev节点,看注释是为了记录下个被删除的节点。
一个布尔类型的red,为true表示该节点为红,为false表示节点为黑。

这个类继承自LinkedHashMap.Entry类,插入时需要传入四个值。

1. balanceInsertion()

/**
 * 红黑树插入节点后,需要重新平衡
 * root 当前根节点
 * x 新插入的节点
 * 返回重新平衡后的根节点
 */
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    x.red = true; // 新插入的节点标为红色
 
    /*
     * 这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
     * xp:当前节点的父节点、xpp:爷爷节点、xppl:左叔叔节点、xppr:右叔叔节点
     */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { 
 
        // 如果父节点为空、说明当前节点就是根节点,那么把当前节点标为黑色,返回当前节点
        if ((xp = x.parent) == null) { // L1
            x.red = false;
            return x;
        }
 
        // 父节点不为空
        // 如果父节点为黑色 或者 【(父节点为红色 但是 爷爷节点为空) -> 这种情况何时出现?】
        else if (!xp.red || (xpp = xp.parent) == null) // L2
            return root;
        if (xp == (xppl = xpp.left)) { // 如果父节点是爷爷节点的左孩子  // L3
            if ((xppr = xpp.right) != null && xppr.red) { // 如果右叔叔不为空 并且 为红色  // L3_1
                xppr.red = false; // 右叔叔置为黑色
                xp.red = false; // 父节点置为黑色
                xpp.red = true; // 爷爷节点置为红色
                x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点 
            }
            else { // 如果右叔叔为空 或者 为黑色 // L3_2
                if (x == xp.right) { // 如果当前节点是父节点的右孩子 // L3_2_1
                    root = rotateLeft(root, x = xp); // 父节点左旋,见下文左旋方法解析
                    xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
                }
                if (xp != null) { // 如果父节点不为空 // L3_2_2
                    xp.red = false; // 父节点 置为黑色
                    if (xpp != null) { // 爷爷节点不为空
                        xpp.red = true; // 爷爷节点置为 红色
                        root = rotateRight(root, xpp);  //爷爷节点右旋,见下文右旋方法解析
                    }
                }
            }
        }
        else { // 如果父节点是爷爷节点的右孩子 // L4
            if (xppl != null && xppl.red) { // 如果左叔叔是红色 // L4_1
                xppl.red = false; // 左叔叔置为 黑色
                xp.red = false; // 父节点置为黑色
                xpp.red = true; // 爷爷置为红色
                x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点 
            }
            else { // 如果左叔叔为空或者是黑色 // L4_2
                if (x == xp.left) { // 如果当前节点是个左孩子 // L4_2_1
                    root = rotateRight(root, x = xp); // 针对父节点做右旋,见下文右旋方法解析
                    xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
                }
                if (xp != null) { // 如果父节点不为空 // L4_2_4
                    xp.red = false; // 父节点置为黑色
                    if (xpp != null) { //如果爷爷节点不为空
                        xpp.red = true; // 爷爷节点置为红色
                        root = rotateLeft(root, xpp); // 针对爷爷节点做左旋
                    }
                }
            }
        }
    }
}

传入根节点的目的是为了检测性质,之后红黑树方法传入根节点root都是这个目的。

  • 首先将插入节点x声名为红色
  • 在for的开头定义了几个工作指针xp,xpp,xppl,xppr,表示x的父,祖父,叔【只可能为左/右一个】
  • 若父节点为空,说明插入根节点,直接变色即可。
  • 接下来是父节点不为空的逻辑:
    • 若父节点为黑【插入规则1】或者祖父为空【父节点是根节点】,直接插入

    • 若父节点为红色,且叔叔节点为红色【插入规则3】,将爷、父、叔分别变色,在递归地处理上面的节点。父亲和叔叔变为黑色,爷变成红色。将爷爷节点当做“新插入的节点”,考虑其爷、父、子三代的变化。

    • 若父节点为黑色,叔叔节点为黑色或者空【插入规则2和 4】,需要分叔叔在左还是在右分别处理:

      • 叔叔在父节点右边:
        在这里插入图片描述
      1. 对x进行右旋【右旋的主角是x,x就能提上去代替父节点的位置】
      2. 父、爷染色
      3. 父节点右旋
      4. 父亲和叔叔置为黑色
      • 在右:
        在这里插入图片描述

【我在这里不对变量名进行改变是为了容易看出变化,实际上在程序运行过程中,会有变量指标随着位置的改变而改变,以达到“名如其实”的效果。
如:xpp = (xp = x.parent) == null ? null : xp.parent;
其次,看上去结果好像由“平衡”变得“不平衡”,实际上只有在真的不平衡的时候才有旋转,其实中间很多子节点都被省略掉了】

2. retateLeft()

左旋、右旋的主角就是移动上去的节点。

 
/**
 * 节点左旋
 * root 根节点
 * p 要左旋的节点
 */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) { // 要左旋的节点以及要左旋的节点的右孩子不为空
        if ((rl = p.right = r.left) != null) // 要左旋的节点的右孩子的左节点 赋给 要左旋的节点的右孩子 节点为:rl
            rl.parent = p; // 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
 
        // 将要左旋的节点的右孩子的父节点  指向 要左旋的节点的父节点,相当于右孩子提升了一层,
        // 此时如果父节点为空, 说明r 已经是顶层节点了,应该作为root 并且标为黑色
        if ((pp = r.parent = p.parent) == null) 
            (root = r).red = false;
        else if (pp.left == p) // 如果父节点不为空 并且 要左旋的节点是个左孩子
            pp.left = r; // 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
        else // 要左旋的节点是个右孩子
            pp.right = r; 
        r.left = p; // 要左旋的节点  作为 他的右孩子的左节点
        p.parent = r; // 要左旋的节点的右孩子  作为  他的父节点
    }
    return root; // 返回根节点
}
 

【像rl = p.right = r.left 表示将l.left赋值给p.right, 并且为了以后表示更方便,用rl来标记这个节点【无论条件成不成立,这个赋值都会进行,因此在其他分支可以直接使用】】

设置几个工作指针,p表示要旋转的父节点,r表示p.right, rl表示r.left。如下图
在这里插入图片描述
第一步,将rl赋值为p.right,即找儿子
第二步,将rl的父亲赋值p,即认父亲【前提是rl不为null,否则NPE】
如下图:【暂时不考虑颜色】
在这里插入图片描述
接下来的几个if - else的图不能看上面的!

  • if :pp = r.parent = p.parent
    别管中间的,只是单纯的赋值而已。pp = p.parent == null,即p就是根节点了此时确保根节点为黑色即可,子节点什么颜色都行。

现在考虑 r.parent = p.parent的意思,即r会认p的父亲为父亲【认父亲】

剩下都表示p不是根节点的情况,pp是不是根节点没什么影响,因为接下来还是要递归处理pp:

  • 若p是pp.left
    if中的赋值使得r已经认了父亲,现在pp需要认r为儿子。r左旋上去,将p给替代了
  • 若p是pp.right
    一样的逻辑,只是把r作为pp的右孩子。

最后,将p作为r的左孩子。【找儿子】
再将p的父亲记作r【认父亲】

如下图的情况:
在这里插入图片描述

很奇怪:好像转了以后更不平衡了,其实并不是,因为我们从未考虑r.right有什么东西,实际上肯定是r.left或者r.right插入了黑节点才会引发左旋,在这之前r应该是平衡的,因此r.right也有东西。
另一种,p为pp左子树的情况:
在这里插入图片描述

在所有操作中,只有p为根节点时候考虑到颜色变化,难道左旋不必考虑颜色吗?

3. retateRight()

/**
 * 节点右旋
 * root 根节点
 * p 要右旋的节点
 */
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) { // 要右旋的节点不为空以及要右旋的节点的左孩子不为空
        if ((lr = p.left = l.right) != null) // 要右旋的节点的左孩子的右节点 赋给 要右旋节点的左孩子 节点为:lr
            lr.parent = p; // 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
 
        // 将要右旋的节点的左孩子的父节点  指向 要右旋的节点的父节点,相当于左孩子提升了一层,
        // 此时如果父节点为空, 说明l 已经是顶层节点了,应该作为root 并且标为黑色
        if ((pp = l.parent = p.parent) == null) 
            (root = l).red = false;
        else if (pp.right == p) // 如果父节点不为空 并且 要右旋的节点是个右孩子
            pp.right = l; // 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
        else // 要右旋的节点是个左孩子
            pp.left = l; // 同上
        l.right = p; // 要右旋的节点 作为 他左孩子的右节点
        p.parent = l; // 要右旋的节点的父节点 指向 他的左孩子
    }
    return root;
}

右旋也是一个道理,只画图了:

前两个if的变化:
在这里插入图片描述
若p为根节点,直接染为黑色。
在这里插入图片描述

若不是,将l右旋上去,替代p的位置:
在这里插入图片描述

三、Map类的方法

HashMap内部节点仍然是Entry,而封装Entry对象的节点有两种:
链表节点Node类,红黑树节点TreeNode类。

因为这种性质,你会经常看到每个方法的逻辑是这样的:
(1)得到首个节点,若他的next为空,就不管是Node还是TreeNode都是一样的做法;
(2)反之,测算他的类型,对其做不同的处理:
TreeNode一般都是封装到一个方法里;
Node就在这个方法就地解决。

1. hash()

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

不再出现大量移位,因为不需要那么复杂了.
h向右移动16位,是为了通过上位hashCode来获取index而不是最末的几位。

2. put() & putVal()

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

put()就是调用一下putVal()


  • 定义几个临时变量:
    tab, 表示HashMap的table数组,类型为Node[],保存每个链表/树的头结点。
    p,工作指针
    n 记录表长tab.length
    i 记录当前操作的tab下标。

  • 判断table有没有初始化,没有则调用resize()方法进行初始化
    resize()会在后面进行说明,这个方法兼顾扩容和初始化的任务。

  • 判断tab的对应当前key的hash下标位置是否为空,为空则直接插入新节点。

  • 若不为空,进行链表或者红黑树的插入位置查找:
    定义几个变量,
    e:存储要插入的节点位置,用于外部的替换。
    k:插入节点的key

若当前节点就已经达到要求,直接将当前节点赋值给p结束。【这里就是为什么是9个作为树化起点的坑。】

若不是,根据节点类型进行迭代:

若p为TreeNode,调用putTreeVal()进行查找,将最终位置赋值给e【这个方法会在后面说明】

若p为链表节点:
定义一个binCount记录查找的链表节点的个数,一直遍历到尾结点,并不断将当前节点赋值给e;
此时将新节点插入到尾结点的后面。
判断此时的binCount是否>=7,若大于等于7则进行树化。
treeifyBin()方法会在后面讲解。

这里有两个注意点,面试可能问到:
(1)1.8的链表插入使用尾插法。
(2)链表节点为9个时,进行树化:
最开始的p就已经是第二个节点了【查看刚开头检查key符不符合那个逻辑】。然后遍历了7趟,都不能满足6 >= 8 - 1【从0到6是七趟】, 此时节点数为8,
第8趟时,再次插入一个新的节点,此时节点数为9,但是binCount == 7,此时进行树化

  • 若还未到尾结点,就找到了与目标key相等的节点,将此时的e跳出循环。

  • 因为工作指针一直是p.next, 并在循环中不断将p.next赋值给e,因此迭代使用p = e

  • 所有情况都考虑上了,已经得到e了,开始进行替换逻辑:
    若e不为null,进行替换:
    记录下原值,替换并返回原值,modCount++;

  • 若所有node的数量之和 > 阈值,调用resize()进行扩容。

3. treeifyBin() & treeify()

/**
 * 将链表节点转为红黑树节点
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 1.如果table为空或者table的长度小于64, 调用resize方法进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 2.根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 3.将链表节点转红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            // 4.如果是第一次遍历,将头节点赋值给hd
            if (tl == null)	// tl为空代表为第一次循环
                hd = p;
            else {
                // 5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
                p.prev = tl;    // 当前节点的prev属性设为上一个节点
                tl.next = p;    // 上一个节点的next属性设置为当前节点
            }
            // 6.将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
            tl = p;
        } while ((e = e.next) != null);
        // 7.将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
/**
 * 构建红黑树
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    // 1.将调用此方法的节点赋值给x,以x作为起点,开始进行遍历
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;   // next赋值为x的下个节点
        x.left = x.right = null;    // 将x的左右节点设置为空
        // 2.如果还没有根节点, 则将x设置为根节点
        if (root == null) {
            x.parent = null;    // 根节点没有父节点
            x.red = false;  // 根节点必须为黑色
            root = x;   // 将x设置为根节点
        }
        else {
            K k = x.key;	// k赋值为x的key
            int h = x.hash;	// h赋值为x的hash值
            Class<?> kc = null;
            // 3.如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                // 4.如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
                if ((ph = p.hash) > h)
                    dir = -1;
                // 5.如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
                else if (ph < h)
                    dir = 1;
                // 6.走到这代表x的hash值和p的hash值相等,则比较key值
                else if ((kc == null && // 6.1 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    // 6.2 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
                    dir = tieBreakOrder(k, pk);
 
                TreeNode<K,V> xp = p;   // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
                // 7.dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    // 8.x和xp节点的属性设置
                    x.parent = xp;  // x的父节点即为最后一次遍历的p节点
                    if (dir <= 0)   // 如果时dir <= 0, 则代表x节点为父节点的左节点
                        xp.left = x;
                    else    // 如果时dir > 0, 则代表x节点为父节点的右节点
                        xp.right = x;
                    // 9.进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 10.如果root节点不在table索引位置的头节点, 则将其调整为头节点
    moveRootToFront(tab, root);
}
  1. treeifyBin()
  • 定义几个变量:
    n :tab.length, 查看是否需要扩容
    index:当前table下标
    e:工作指针,最终返回插入位置

  • 并不会马上树化,优先选择扩容【只有table.length达到最大table的长度才进行树化】
    为什么扩容可以代替树化?
    扩容之后,表的散列型理论上会更好,因此链表的长度会缩短,此时冲突率也会降低,因此效率提升了
    若节点不是很多,选择树化反而更加耗时。

  • 遍历每个链表节点,将每个节点转为TreeNode,并将table[index]对应的头节点存储到hd中。
    这里声明了两个变量:
    hd:记录首节点;
    tl:记录上一个节点,用于生成双向链表。

看看这个替换节点的方法:
在这里插入图片描述

很简单的方法,利用传入的原Node属性替换为TreeNode的属性即可。

  • 对于第一个之后的节点,进行双向链表化:
    将其prev指针指向前面,将next指针指向下一个

两个注意点:
TreeNode为什么会有next属性?
TreeNode继承自Entry类,拥有父类的next属性;

prev与parent指针有什么区别?
prev指向的是链表的上一个节点,parent指的是树节点的父节点。
链表的上一个节点很有可能不是树节点的父节点,两者完全没有任何关系

  • tl代表下个节点的前节点,直接替换p即可。
  • 将tab[index]的头指针关联到链表头结点,调用treeify()对整个table【index】进行树化。

双向链表示意图:
在这里插入图片描述


treeify():【将下标处的table[index]的TreeNode的双向链表转化为数组】

  • root用来存储该树的根节点
  • 遍历整个链表,工作指针使用x【实际插入的节点】,p【查找插入位置】
  • 当root == null,表示当前是第一个节点,将其赋值给root【链表的第一个节点不一定是树根】
  • 若root != null,表示当前在遍历之后的节点,需要根据比较来确定下个节点插入在左子树还是右子树。

声名的几个变量:
k: 当前节点的key
h:当前节点hash
kc:当前节点所属的类变量。【查看有无Comparable】
dir:表示比较结果,使用-1,0,1这种compare()式作为结果
ph:工作指针p的hash值
pk:p的key,用于第三轮比较

比较规则有四种:

①比较hash,这是TreeNode的一个属性,通过特有的hash()函数对原来的hashCode方法进行改造得来的【移位、异或】
②使用Comparable接口的compareTo()方法比较
comparableClassFor():

static Class<?> comparableClassFor(Object x) {
    // 1.判断x是否实现了Comparable接口
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        // 2.校验x是否为String类型
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            // 3.遍历x实现的所有接口
            for (int i = 0; i < ts.length; ++i) {
                // 4.如果x实现了Comparable接口,则返回x的Class
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

comparableClassFor()方法:

  • 判断x是否实现Comparable接口,若不符合,直接返回null;
  • 若x为String,直接返回String.class
  • 得到x实现的所有接口【getGenericInterfaces(), 得到Type[],表示得到的接口类型即泛型信息】
  • 遍历Type[],若含有Comparable即返回x.class
  • (ParameterizedType)t).getRawType()表示得到<>前面的内容。
  • p.getActualTypeArguments()表示得到<>中间的内容

比较方式【续②】:
若得到的x.class不为空,表名实现了Comparable,调用其compareTo()方法,即compareComparables()

③tieBreakOrder(),包含两种比较方式:
(1)比较class的名称顺序【String的比较】;
(2)比较identitiHashCode(),这个方法表示通过Object类原生的hashCode()方法得到的哈希值,因为hashCode()方法很有可能被子类重写。

// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。
static int tieBreakOrder(Object a, Object b) {  
    int d;
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}

几个易混属性的比较

这里给出我对这个解容易弄混的概念的分析:
(1)Node类的hash属性:
在这里插入图片描述
表示通过key的.hashCode方法经过高低位的异或来得到的,可以认为是当前K类的hashCode()的加工品。
(2)key:
key就是平时调用put()传入的第一个参数,通过调用hash()就能得到对应的node类的hash,用于插入table下标的计算【注意,即使key不相等,也有可能得到同样的key.hash(),也就可以插入相同的table数组的下标,因此两个节点插入到一条链表上不能说真的哈希冲突了,只能说这是node类的“hash冲突了”,究其原因hash()只使用其中的8位进行运算【若扩容会有所增长】】
(3)HashMap节点的hashCode():
有时候类会重写Object类的hashCode(),得到的结果往往用于equals()方法的比较。
在这里插入图片描述
Node类重写的hashCode()是将传入的key和value的值取hashCode()再异或。
(4)identityHashCode():即Object类原本的hashCode(),怎么计算的我也不知道,好像和内存地址有关,因为这个hashCode()基本不可能出现重复,因此称为“identity【识别身份使用的】HashCode”也就可以理解了。


  • 上面已经提出了四种不同的比价方式,按照顺序,若不为0就不必进行后面几轮的比较了,现在获取到了dir值了

  • 若dir <= 0, 将查找p的左节点,若dir > 0 查找p的右节点
    使用xp记录父节点,用于插入x,xp会记录p最后一次不为null时候的值

  • 调用红黑树的插入方法进行x的插入;

  • 调整root的位置为table中头指针指向
    在调整平衡树的过程中,可能因为旋转导致root节点偏移到了左子树或者右子树,因此要将table上原来的头指针改变指向。【这个方法看上去挺长。。。】

/**
 * 将root放到头节点的位置
 * 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
 * 将root放到头节点的位置, 原头节点放在root的next节点上
 */
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    // 1.校验root是否为空、table是否为空、table的length是否大于0
    if (root != null && tab != null && (n = tab.length) > 0) {
        // 2.计算root节点的索引位置
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        // 3.如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
        if (root != first) {
            Node<K,V> rn;
            // 3.1 将该索引位置的头节点赋值为root节点
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;   // root节点的上一个节点
            // 3.2 和 3.3 两个操作是移除root节点的过程
            // 3.2 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            // 3.3 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
            if (rp != null)
                rp.next = rn;
            // 3.4 和 3.5 两个操作将first节点接到root节点后面
            // 3.4 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
            if (first != null)
                first.prev = root;
            // 3.5 将root节点的next属性设置为原头节点
            root.next = first;
            // 3.6 root此时已经被放到该位置的头节点位置,因此将prev属性设为空
            root.prev = null;
        }
        // 4.检查树是否正常
        assert checkInvariants(root);
    }
}

这里面实际用上了之前的双向链表的next与prev指针,若root != first, 就查找到root的位置,替换原来first的位置。
最后使用断言查看树是否符合要求:【断言需要开启启动参数-ea才能生效,因此此句可能不执行】

/**
 * Recursive invariant check
 */
static <K,V> boolean checkInvariants(TreeNode<K,V> t) { // 一些基本的校验
    TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
        tb = t.prev, tn = (TreeNode<K,V>)t.next;
    if (tb != null && tb.next != t)
        return false;
    if (tn != null && tn.prev != t)
        return false;
    if (tp != null && t != tp.left && t != tp.right)
        return false;
    if (tl != null && (tl.parent != t || tl.hash > t.hash))
        return false;
    if (tr != null && (tr.parent != t || tr.hash < t.hash))
        return false;
    if (t.red && tl != null && tl.red && tr != null && tr.red)  // 如果当前节点为红色, 则该节点的左右节点不能同时为红色
        return false;
    if (tl != null && !checkInvariants(tl))
        return false;
    if (tr != null && !checkInvariants(tr))
        return false;
    return true;
}

递归地对树的每个节点的指针进行检查,若一个节点指针不正确,程序就会异常停止。

4. resize()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1.老表的容量不为0,即老表不为空
    if (oldCap > 0) {
        // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
        // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  // 将索引值为j的老表头节点赋值给e
                oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
                // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 9.如果是普通的链表节点,则进行普通的重hash分布
                    Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                loHead = e; // 则将loHead赋值为第一个节点
                            else
                                loTail.next = e;    // 否则将节点添加在loTail后面
                            loTail = e; // 并将loTail赋值为新增的节点
                        }
                        // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {
                            if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                hiHead = e; // 则将hiHead赋值为第一个节点
                            else
                                hiTail.next = e;    // 否则将节点添加在hiTail后面
                            hiTail = e; // 并将hiTail赋值为新增的节点
                        }
                    } while ((e = next) != null);
                    // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                    // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                    // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 12.返回新表
    return newTab;
}

又是个罗里吧嗦的方法。

几个变量:
oldCap:原数组的容量【非常有用】
oldThr:原来阈值;
newCap:新容量
newThr:。。。

  • 若oldCap > 0,表示原table不为空,才考虑扩容或者初始化
    • 若oldCap大于最大的容量,将阈值拉大无穷大,表示已经无法扩容【之后无论怎么添加都打不到阈值,就不会触发resize()了】
    • 将oldCap增加一倍,赋值给newCap,若仍不超过最大阈值,改变新的阈值【即将原来阈值 * 2】
  • 若oldCap == 0,表示原table没有经过初始化,即要进行初始化的逻辑
    • 设置cap和thr都为类的默认值
    • 【为什么会出现默认阈值为0?】若thr == 0,使用 容量 * 负载因子得到thr
    • 初始化新table,将算出来的属性赋值给他。

若oldTable不为空,说明这是扩容的逻辑【若是初始化逻辑,此时不会进入这个if分支】

  • 进入循环,e为工作指针,初始化为对应下标j的头结点。
  • 将原来的头结点的指针清空
  • 若e.next == null,表示table[j]只有一个链表节点,直接结束【表还是树都不要紧】。
  • 判断e节点的类型,若是树,调用split(后面讲)
  • 若是链表节点,定义几个工作指针用于循环执行:
    loHead:表示插入新链表下标为j的头结点;
    loTail:表示插入新链表下标为j的尾结点;
    hiTail:表示插入新链表下标为j + oldcap的尾结点;
    hiHead:表示插入新链表下标为j + oldCap的头结点;
    next:e的下个节点

老table与新table数组下标的呼应

这里说一下为什么要这么设计:
对于这种按照2的整数次幂计算的数组容量,且使用对应数位的hash码【如若表长为16,则采用4为hash计算下标】的计算方式,若扩容的新容量刚好为原来的两倍,则在原来的数组中位置为j的节点,在新送祝福中只可能有两种下标:
(1)若e.hash & oldCap == 0, 则j对应的新数组的下标也是j
(2)若e.hash & oldCap != 0,则j对应的新数组下标为j + oldCap
因为oldCap必为2的整数次幂,因此只有可能其中一个数位为0,因此与出来的结果只可能为0或者不为0
为什么会这样,我也不是很清楚。估计这是那些设计者之所以非要设计成2的整数倍的原因吧【快捷的位运算也是】

分析之后,后面的逻辑就很简单了。
若与出结果为0,将e挂到loHead为头结点的链表上;
若不为0,挂到hiHead为头结点的链表上,
之后直接将这两个链表挂到table的j和j + oldCap的位置。

最后,将尾结点的next设为null,并初始化链表对应位置,并设链表头结点为table[lo]和table[hi]的引用。

三种扩容的节点转移方式

到目前为止,已经有三种扩容的插入方法了,每种各有特点:
1.7的HashMap,采取一个节点结算一个新的index,算完之后深拷贝节点直接挂过去【采取头插法】;【转移次数 == size()】《需要new》
1.7的ConcurrentHashMap:采取一次转移多个相同index的相邻节点的方式:当数组i的位置出现一个节点e对应新数组的下标j的位置,他会检查这个e的相邻的几个子孙,将与其具有相同index = j的全部深拷贝,挂到一起在转移过去【转移次数不止常数次,且没有size()次】《需要new》
1.8的HashMap,采取分类的方式,设计只有两种可能链表头部作为头结点,将符合这个下标的一次挂到这个位置,这是修改指针的方式,不需要创建新节点【转移次数为常数次,即oldCap * 2次】《无需new》


5. split()

/**
 * 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;	// 拿到调用此方法的节点
    TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
    TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
    int lc = 0, hc = 0;
    // 1.以调用此方法的节点开始,遍历整个红黑树节点
    for (TreeNode<K,V> e = b, next; e != null; e = next) {	// 从b节点开始遍历
        next = (TreeNode<K,V>)e.next;   // next赋值为e的下个节点
        e.next = null;  // 同时将老表的节点设置为空,以便垃圾收集器回收
        // 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)  // 如果loTail为空, 代表该节点为第一个节点
                loHead = e; // 则将loHead赋值为第一个节点
            else
                loTail.next = e;    // 否则将节点添加在loTail后面
            loTail = e; // 并将loTail赋值为新增的节点
            ++lc;   // 统计原索引位置的节点个数
        }
        // 3.如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
        else {
            if ((e.prev = hiTail) == null)  // 如果hiHead为空, 代表该节点为第一个节点
                hiHead = e; // 则将hiHead赋值为第一个节点
            else
                hiTail.next = e;    // 否则将节点添加在hiTail后面
            hiTail = e; // 并将hiTail赋值为新增的节点
            ++hc;   // 统计索引位置为原索引+oldCap的节点个数
        }
    }
    // 4.如果原索引位置的节点不为空
    if (loHead != null) {   // 原索引位置的节点不为空
        // 4.1 如果节点个数<=6个则将红黑树转为链表结构
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            // 4.2 将原索引位置的节点设置为对应的头节点
            tab[index] = loHead;
            // 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (hiHead != null)
                // 4.4 以loHead为根节点, 构建新的红黑树
                loHead.treeify(tab);
        }
    }
    // 5.如果索引位置为原索引+oldCap的节点不为空
    if (hiHead != null) {   // 索引位置为原索引+oldCap的节点不为空
        // 5.1 如果节点个数<=6个则将红黑树转为链表结构
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            // 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点
            tab[index + bit] = hiHead;
            // 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (loHead != null)
                // 5.4 以hiHead为根节点, 构建新的红黑树
                hiHead.treeify(tab);
        }
    }
}

这个代码看上去很长,其实有很多重复的逻辑。


创建几个变量:
e :工作指针,初始化 e = b = this,表示调用的这个节点
lc:低下标的节点个数
hc:高下标的节点个数

剩下的几个是不是很熟悉?对,就是照抄之前的rehash()的做法【此处bit也表示oldTable的cap】
不同点在于,每次循环的过程中需要额外计算链表长度

为什么可以?为什么要转链表?
可以:TreeNode由于具有next和prev指针,因此可以说就是一个双向链表,具有线性和树形两种结构,也能使用链表的逻辑;
要转:对于节点数量低来说,插入、删除链表的速度可比红黑树快多了,因此若不超过阈值的话,优先选择分裂成链表

  • 得到高低两个链表后,通过之前lc来判断是否达到转化红黑树的个数阈值,若未达到,解除该链表的树性质【untreeify()】,即链表化
/**
 * 将红黑树节点转为链表节点, 当节点<=6个时会被触发
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点
    // 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点
    for (Node<K,V> q = this; q != null; q = q.next) {
        // 2.调用replacementNode方法构建链表节点
        Node<K,V> p = map.replacementNode(q, null);
        // 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
        if (tl == null)
            hd = p;
        // 4.否则, 将尾节点的next属性设置为当前节点p
        else
            tl.next = p;
        tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点
    }
    // 6.返回转换后的链表的头节点
    return hd;
}

逻辑很简单,按照原来的TreeNode隐藏的双向链表性质进行遍历,将每个TreeNode的属性取出来,并赋值给new的Node并替换,最后将头结点返回。

若大于阈值,需要转为红黑树。
对于hiHead对应的那半边数组也是一样的逻辑,只是下标需要从index转为index + bit

左边没有就不管了
在这里插入图片描述

6.get()与getNode()

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
 
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1.对table进行校验:table不为空 && table长度大于0 && 
    // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 6.找不到符合的返回空
    return null;
}

get():
直接调用getNode(),传入key对应的hash()与key


getNode():

检查由该hash计算出来的节点的数组下标元素的hash是否符合传入地hash,符合进入此下标指向的链表/红黑树查找。

还是老套的逻辑:
先检查是不是只有一个节点或者首节点的key就是这个key;
在检查节点类型,若是TreeNode的节点,进入getTreeNode执行任务。
若是普通链表节点,进行迭代查找,未找到返回null。

7.getTreeNode()与find()

final TreeNode<K,V> getTreeNode(int h, Object k) {
    // 1.首先找到红黑树的根节点;2.使用根节点调用find方法
    return ((parent != null) ? root() : this).find(h, k, null);
}

/**
 * 从调用此方法的节点开始查找, 通过hash值和key找到对应的节点
 * 此方法是红黑树节点的查找, 红黑树是特殊的自平衡二叉查找树
 * 平衡二叉查找树的特点:左节点<根节点<右节点
 */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    // 1.将p节点赋值为调用此方法的节点,即为红黑树根节点
    TreeNode<K,V> p = this;
    // 2.从p节点开始向下遍历
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        // 3.如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h) // 4.如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
            p = pr;
        // 5.如果传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if (pl == null)    // 6.p节点的左节点为空则将向右遍历
            p = pr;
        else if (pr == null)    // 7.p节点的右节点为空则向左遍历
            p = pl;
        // 8.将p节点与k进行比较
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) && // 8.1 kc不为空代表k实现了Comparable
                 (dir = compareComparables(kc, k, pk)) != 0)// 8.2 k<pk则dir<0, k>pk则dir>0
            // 8.3 k<pk则向左遍历(p赋值为p的左节点), 否则向右遍历
            p = (dir < 0) ? pl : pr;
        // 9.代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
        else if ((q = pr.find(h, k, kc)) != null) 
            return q;
        // 10.代码走到此处代表“pr.find(h, k, kc)”为空, 因此直接向左遍历
        else
            p = pl;
    } while (p != null);
    return null;
}

直接调用find()方法, 传入的值为根节点【若parent == null,当前节点就是根节点;若不为null,调用root()查找根节点。】
在这里插入图片描述
root()是个逻辑简单的方法,一直向上迭代,直到没有parent的那个树节点返回。


find():
类似二叉搜索树的查找,不断比较左右节点的key与当前节点的k的大小,若小于进入左子树,大于进入右子树【二分查找】,若key == p.key或者k.equals(pk),直接返回p;
若无法直接比较,得到节点的类型若实现了Comparable就进行比较,若未实现,比较类名;还不能比较原生hashCode()
dir == 0,返回true。
等于null,返回null

8. remove()

/**
 * 移除某个节点
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
 
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 3.否则将p.next赋值给e,向下遍历节点
            // 3.1 如果p是TreeNode则调用红黑树的方法查找节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 3.2 否则,进行普通链表节点的查找
                do {
                    // 当节点的hash值和key与传入的相同,则该节点即为目标节点
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;	// 赋值给node, 并跳出循环
                        break;
                    }
                    p = e;  // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
                } while ((e = e.next) != null); // e指向下一个节点
            }
        }
        // 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 4.1 如果是TreeNode则调用红黑树的移除方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
            // “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
            else if (node == p)
                tab[index] = node.next;
            // 4.3 否则将node的上一个节点的next属性设置为node的next节点,
            // 即将node节点移除, 将node的上下节点进行关联(链表的移除)
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node); // 供LinkedHashMap使用
            // 5.返回被移除的节点
            return node;
        }
    }
    return null;
}

remove()分为两步,第一步查找到节点,和前面的get()一毛一样。
接下来进行节点删除【只有一个节点的情况在第一个逻辑中直接删除了】
TreeNode的删除使用额外的方法removeTreeNode,普通节点的删除直接修改next指针即可,注意若为头结点需要修改table的指针。

9. removeTreeNode()

/**
 * 红黑树的节点移除
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    // --- 链表的处理start ---
    int n;
    // 1.table为空或者length为0直接返回
    if (tab == null || (n = tab.length) == 0)
        return;
    // 2.根据hash计算出索引的位置
    int index = (n - 1) & hash;
    // 3.将索引位置的头节点赋值给first和root
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
    // 将node的next节点赋值给succ节点,prev节点赋值给pred节点
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 5.如果pred节点为空,则代表要被移除的node节点为头节点,
    // 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
    if (pred == null)
        tab[index] = first = succ;
    else
        // 6.否则将pred节点的next属性设置为succ节点(node的next节点)
        pred.next = succ;
    // 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
    if (succ != null)
        succ.prev = pred;
    // 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
    if (first == null)
        return;
    // 9.如果root的父节点不为空, 则将root赋值为根节点
    if (root.parent != null)
        root = root.root();
    // 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
    // (转链表后就无需再进行下面的红黑树处理)
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // --- 链表的处理end ---
 
    // --- 以下代码为红黑树的处理 ---
    // 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    // 12.如果p的左节点和右节点都不为空时
    if (pl != null && pr != null) {
        // 12.1 将s节点赋值为p的右节点
        TreeNode<K,V> s = pr, sl;
        // 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
        while ((sl = s.left) != null)
            s = sl;
        // 12.3 交换p节点和s节点的颜色
        boolean c = s.red; s.red = p.red; p.red = c;
        TreeNode<K,V> sr = s.right; // s的右节点
        TreeNode<K,V> pp = p.parent;    // p的父节点
        // --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
        // 12.4 第一次调整
        // 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
        if (s == pr) {
            p.parent = s;
            s.right = p;
        }
        else {
            // 将sp赋值为s的父节点
            TreeNode<K,V> sp = s.parent;
            // 将p的父节点赋值为sp
            if ((p.parent = sp) != null) {
                // 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
                if (s == sp.left)
                    sp.left = p;
                // 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
                else
                    sp.right = p;
            }
            // s的右节点赋值为p节点的右节点
            if ((s.right = pr) != null)
                // 如果pr不为空,则将pr的父节点赋值为s
                pr.parent = s;
        }
        // 12.5 第二次调整
        // 将p的左节点赋值为空,pl已经保存了该节点
        p.left = null;
        // 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
        if ((p.right = sr) != null)
            sr.parent = p;
        // 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
        if ((s.left = pl) != null)
            pl.parent = s;
        // 将s的父节点赋值为p的父节点pp
        // 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
        if ((s.parent = pp) == null)
            root = s;
        // 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
        else if (p == pp.left)
            pp.left = s;
        // 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
        else
            pp.right = s;
        // 12.6 寻找replacement节点,用来替换掉p节点
        // 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
        if (sr != null)
            replacement = sr;
        // 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
        else
            replacement = p;
    }
    // 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
    else if (pl != null)
        replacement = pl;
    // 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
    else if (pr != null)
        replacement = pr;
    // 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
    else
        replacement = p;
    // 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
    if (replacement != p) { // 如果p节点不是叶子节点
        // 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
        TreeNode<K,V> pp = replacement.parent = p.parent;
        // 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
        if (pp == null)
            root = replacement;
        // 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
        else if (p == pp.left)
            pp.left = replacement;
        // 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
        else
            pp.right = replacement;
        // 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
        p.left = p.right = p.parent = null;
    }
    // 17.如果p节点不为红色则进行红黑树删除平衡调整
    // (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
 
    // 18.如果p节点为叶子节点, 则简单的将p节点去除即可
    if (replacement == p) {
        TreeNode<K,V> pp = p.parent;
        // 18.1 将p的parent属性设置为空
        p.parent = null;
        if (pp != null) {
            // 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
            if (p == pp.left)
                pp.left = null;
            // 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
            else if (p == pp.right)
                pp.right = null;
        }
    }
    if (movable)
        // 19.将root节点移到索引位置的头节点
        moveRootToFront(tab, r);
}

红黑树的删除逻辑太过复杂,以后再看。
这里重点看一下链表化的条件:
在这里插入图片描述
与之前的扩容不同,不是判段节点的数量有没有到达阈值,而是看根节点是否存在、左右子树是否存在、左子树的左子树是否存在,左子树的左子树是否存在,任何条件只要不存在一个就退化为链表。

其实,若满足这些条件,节点数量必定不会太多(约10到13个以内)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值