史上最详细的 JDK 1

代码块9:checkInvariants

resize 方法

代码块10:split

代码块11:untreeify

例子1:扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?

remove 方法

代码块12:removeTreeNode

解释1:为什么 sr 是 replacement 的首选,p 为备选?

图解1:removeTreeNode 图解

解释2:关于红黑树的平衡调整?

死循环问题

JDK 1.8扩容过程

具体扩容过程:

HashMap 和 Hashtable 的区别

总结

参考


前言

==

JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”,本文就 HashMap 的几个常用的重要方法和 JDK 1.8 之前的死循环问题展开学习讨论。

JDK 1.8 的 HashMap 的数据结构如下图所示,当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会转为红黑树。

本文地址:http://blog.csdn.net/v123411739/article/details/78996181

几个点:

========

先了解以下几个点,有利于更好的理解 HashMap 的源码和阅读本文。

1、本文中头节点指的是 table 表上索引位置的节点,也就是链表的头节点。

2、根节点(root 节点)指的是红黑树最上面的那个节点,也就是没有父节点的节点。

3、红黑树的根节点不一定是索引位置的头节点(也就是链表的头节点),HashMap 通过 moveRootToFront 方法来维持红黑树的根结点就是索引位置的头结点,但是在 removeTreeNode 方法中,当 movable 为 false 时,不会调用 moveRootToFront 方法,此时红黑树的根节点不一定是索引位置的头节点,该场景发生在 HashIterator 的 remove 方法中。

4、转为红黑树节点后,链表的结构还存在,通过 next 属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。

5、在红黑树上,叶子节点也可能有 next 节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有 next 节点。

6、源码中一些变量定义:如果定义了一个节点 p,则 pl(p left)为 p 的左节点,pr(p right)为 p 的右节点,pp(p parent)为 p 的父节点,ph(p hash)为 p 的 hash 值,pk(p key)为 p 的 key 值,kc(key class)为 key 的类等等。源码中很喜欢在 if/for 等语句中进行赋值并判断,请注意。

7、链表中移除一个节点只需如下图操作,其他操作同理。

8、红黑树在维护链表结构时,移除一个节点只需如下图操作(红黑树中增加了一个 prev 属性),其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作。

9、源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的 hash 值小于 p 节点的 hash 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。2)如果目标节点的 key 值小于 p 节点的 key 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。这两条规则是利用了红黑树的特性(左节点 < 根节点 < 右节点)。

10、源码中进行红黑树的查找时,会用 dir(direction)来表示向左还是向右查找,dir 存储的值是目标节点的 hash/key 与 p 节点的 hash/key 的比较结果。

基本属性

========

// 默认容量16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认负载因子0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表节点转换红黑树节点的阈值, 9个节点转

static final int TREEIFY_THRESHOLD = 8;

// 红黑树节点转换链表节点的阈值, 6个节点转

static final int UNTREEIFY_THRESHOLD = 6;

// 转红黑树时, table的最小长度

static final int MIN_TREEIFY_CAPACITY = 64;

// 链表节点, 继承自Entry

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;

final K key;

V value;

Node<K,V> next;

// … …

}

// 红黑树节点

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

TreeNode<K,V> parent; // red-black tree links

TreeNode<K,V> left;

TreeNode<K,V> right;

TreeNode<K,V> prev; // needed to unlink next upon deletion

boolean red;

// …

}

定位哈希桶数组索引位置

===========

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过 HashMap 的数据结构是“数组+链表+红黑树”的结合,所以我们当然希望这个 HashMap 里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。HashMap 定位数组索引位置,直接决定了 hash 方法的离散性能。下面是定位哈希桶数组的源码:

// 代码1

static final int hash(Object key) { // 计算key的hash值

int h;

// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

// 代码2

int n = tab.length;

// 将(tab.length - 1) 与 hash值进行&运算

int index = (n - 1) & hash;

整个过程本质上就是三步:

  1. 拿到 key 的 hashCode 值

  2. 将 hashCode 的高位参与运算,重新计算 hash 值

  3. 将计算出来的 hash 值与 (table.length - 1) 进行 & 运算

方法解读:

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。

但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此 JDK 团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率。

在 JDK1.8 的实现中,还优化了高位运算的算法,将 hashCode 的高 16 位与 hashCode 进行异或运算,主要是为了在 table 的 length 较小的时候,让高位也参与运算,并且不会有太大的开销。

下图是一个简单的例子:

当 table 长度为 16 时,table.length - 1 = 15 ,用二进制来看,此时低 4 位全是 1,高 28 位全是 0,与 0 进行 & 运算必然为 0,因此此时 hashCode 与 “table.length - 1” 的 & 运算结果只取决于 hashCode 的低 4 位,在这种情况下,hashCode 的高 28 位就没有任何作用,并且由于 hash 结果只取决于 hashCode 的低 4 位,hash 冲突的概率也会增加。因此,在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率。

get 方法

==========

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;

}

4.如果是红黑树节点,则调用红黑树的查找目标节点方法 getTreeNode,见代码块1详解

代码块1:getTreeNode


final TreeNode<K,V> getTreeNode(int h, Object k) {

// 1.首先找到红黑树的根节点;2.使用根节点调用find方法

return ((parent != null) ? root() : this).find(h, k, null);

}

2.使用根节点调用 find 方法,见代码块2详解

代码块2:find


/**

  • 从调用此方法的节点开始查找, 通过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;

}

8.将 p 节点与 k 进行比较。如果传入的 key(即代码中的参数 k)所属的类实现了 Comparable 接口(kc 不为空,comparableClassFor 方法见代码块3详解),则将 k 跟 p 节点的 key 进行比较(kc 实现了 Comparable 接口,因此通过 kc 的比较方法进行比较),并将比较结果赋值给 dir,如果 dir<0 则代表 k<pk,则向 p 节点的左边遍历(pl);否则,向 p 节点的右边遍历(pr)。

代码块3: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;

}

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) {

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;

}

1.校验 table 是否为空或者 length 等于0,如果是则调用 resize 方法进行初始化,见resize方法详解**。**

4.如果 p 节点不是目标节点,则判断 p 节点是否为 TreeNode,如果是则调用红黑树的 putTreeVal 方法查找目标节点,见代码块4详解**。**

7.校验节点数是否超过 8 个,如果超过则调用 treeifyBin方法 将链表节点转为红黑树节点,见代码块6详解

代码块4:putTreeVal


/**

  • 红黑树的put操作,红黑树插入会同时维护原来的链表属性, 即原来的next属性

*/

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,

int h, K k, V v) {

Class<?> kc = null;

boolean searched = false;

// 1.查找根节点, 索引位置的头节点并不一定为红黑树的根节点

TreeNode<K,V> root = (parent != null) ? root() : this;

// 2.将根节点赋值给p节点,开始进行查找

for (TreeNode<K,V> p = root;😉 {

int dir, ph; K pk;

// 3.如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树

if ((ph = p.hash) > h)

dir = -1;

// 4.如果传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右边查找树

else if (ph < h)

dir = 1;

// 5.如果传入的hash值和key值等于p节点的hash值和key值, 则p节点即为目标节点, 返回p节点

else if ((pk = p.key) == k || (k != null && k.equals(pk)))

return p;

// 6.如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等

else if ((kc == null &&

(kc = comparableClassFor(k)) == null) ||

(dir = compareComparables(kc, k, pk)) == 0) {

// 6.1 第一次符合条件, 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回

if (!searched) {

TreeNode<K,V> q, ch;

searched = true;

if (((ch = p.left) != null &&

(q = ch.find(h, k, kc)) != null) ||

((ch = p.right) != null &&

(q = ch.find(h, k, kc)) != null))

return q;

}

// 6.2 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找

dir = tieBreakOrder(k, pk); // dir<0则代表k<pk,则向p左边查找;反之亦然

}

TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值

// 7.dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置

if ((p = (dir <= 0) ? p.left : p.right) == null) {

// 走进来代表已经找到x的位置,只需将x放到该位置即可

Node<K,V> xpn = xp.next; // xp的next节点

// 8.创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间

TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);

// 9.调整x、xp、xpn之间的属性关系

if (dir <= 0) // 如果时dir <= 0, 则代表x节点为xp的左节点

xp.left = x;

else // 如果时dir> 0, 则代表x节点为xp的右节点

xp.right = x;

xp.next = x; // 将xp的next节点设置为x

x.parent = x.prev = xp; // 将x的parent和prev节点设置为xp

// 如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应

if (xpn != null)

((TreeNode<K,V>)xpn).prev = x;

// 10.进行红黑树的插入平衡调整

moveRootToFront(tab, balanceInsertion(root, x));

return null;

}

}

}

6.1 第一次符合条件,从 p 节点的左节点和右节点分别调用 find 方法(见代码块2详解)进行查找,如果查找到目标节点则返回

6.2 否则使用定义的一套规则来比较 k 和 p 节点的 key 的大小,用来决定向左还是向右查找,见代码块5详解

10.进行红黑树的插入平衡调整,见文末的解释2

代码块5:tieBreakOrder


// 用于不可比较或者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;

}

定义一套规则用于极端情况下比较两个参数的大小。

代码块6:treeifyBin


/**

  • 将链表节点转为红黑树节点

*/

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);

}

}

7.将 table 该索引位置赋值为新转的 TreeNode 的头节点 hd,如果该节点不为空,则以 hd 为根节点,构建红黑树,见代码块7详解

代码块7:treeify


/**

  • 构建红黑树

*/

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);

}

3.如果当前节点 x 不是根节点, 则从根节点开始查找属于该节点的位置,该段代码跟代码块2代码块4的查找代码类似。

8.如果 root 节点不在 table 索引位置的头节点, 则将其调整为头节点,见代码块8详解

代码块8:moveRootToFront


/**

  • 将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);

}

}

4.检查树是否正常,见代码块9详解

代码块9:checkInvariants


/**

  • 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;

}

将传入的节点作为根节点,遍历所有节点,校验节点的合法性,主要是保证该树符合红黑树的规则。

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;

}

2.老表的容量为 0,并且老表的阈值大于 0:这种情况是新建 HashMap 时传了初始容量,例如:new HashMap<>(32),使用这种方式新建 HashMap 时,由于 HashMap 没有 capacity 属性,所以此时的 capacity 会被暂存在 threshold 属性。因此此时的 threshold 的值就是我们要新创建的 HashMap 的 capacity,所以将新表的容量设置为 threshold。

4.如果新表的阈值为空,则通过新的容量 * 负载因子获得阈值(这种情况是初始化的时候传了初始容量,跟第2点相同情况,或者初始容量设置的太小导致老表的容量没有超过 16 导致的)。

8.如果是红黑树节点,则进行红黑树的重 hash 分布,见代码块10详解

9.1 如果 e 的 hash 值与老表的容量进行位与运算为 0,则说明 e 节点扩容后的索引位置跟老表的索引位置一样(见例子1详解),进行链表拼接操作:如果 loTail 为空,代表该节点为第一个节点,则将 loHead 赋值为该节点;否则将节点添加在 loTail 后面,并将 loTail 赋值为新增的节点。

9.2 如果 e 的 hash 值与老表的容量进行位与运算为 1,则说明 e 节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1详解),进行链表拼接操作:如果 hiTail 为空,代表该节点为第一个节点,则将 hiHead 赋值为该节点;否则将节点添加在 hiTail 后面,并将 hiTail 赋值为新增的节点。

代码块10: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);

}

}

}

2.如果 e 的 hash 值与老表的容量进行位与运算为 0,则说明 e 节点扩容后的索引位置跟老表的索引位置一样(见例子1详解),进行链表拼接操作:如果 loTail 为空,代表该节点为第一个节点,则将 loHead 赋值为该节点;否则将节点添加在 loTail 后面,并将 loTail 赋值为新增的节点,并统计原索引位置的节点个数。

3.如果 e 的 hash 值与老表的容量进行位与运算为 1,则说明 e 节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1详解),进行链表拼接操作:如果 hiTail 为空,代表该节点为第一个节点,则将 hiHead 赋值为该节点;否则将节点添加在 hiTail 后面,并将 hiTail 赋值为新增的节点,并统计索引位置为原索引 + oldCap 的节点个数。

4.1 如果节点个数 <= 6 个则将红黑树转为链表结构,见代码块11详解

4.4 以 loHead 为根节点,构建新的红黑树,见代码块7详解

代码块11: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;

}

例子1:扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习

已经将知识体系整理好(源码,笔记,PPT,学习视频)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
点,构建新的红黑树,见代码块7详解

代码块11: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;

}

例子1:扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-CCjAW8Fs-1713582544930)]

[外链图片转存中…(img-JoA84vUA-1713582544930)]

[外链图片转存中…(img-Tvz1WVBE-1713582544931)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习

已经将知识体系整理好(源码,笔记,PPT,学习视频)

[外链图片转存中…(img-5SLxFhCz-1713582544931)]

[外链图片转存中…(img-vWePma1L-1713582544931)]

[外链图片转存中…(img-i88hiHS4-1713582544931)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值