JDK1.8中的HashMap
Posted 愉悦滴帮主)
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK1.8中的HashMap相关的知识,希望对你有一定的参考价值。
目录
问题1:为什么要将1.7中HashMap的链表结构改为红黑树?
JDK1.8中的HashMap
相比JDK1.7中的HashMap的最大区别就是在JDK1.8中HashMap中的数组+链表结构变为了数组+链表+红黑树。
问题1:为什么要将1.7中HashMap的链表结构改为红黑树?
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时也就是链表过长,那么通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。
在JDK1.6,JDK1.7中,HashMap的扩容条件是当元素个数大于阈值而且产生hash冲突。那么在不产生hash冲突且大于阈值的时候,就会导致位桶上的链表过长,查询速度变慢。而JDK1.8中,HashMap的扩容条件改为元素个数大于阈值时就产生扩容并且将链表转为红黑树这样做既增加了查询速度,又减少了每个位桶上对应链表或树的长度。
问题2:为什么HashMap要用红黑树?红黑树好在哪里。
首先我们要理解什么是红黑树:红黑树是平衡二叉查找树的一种。为了深入了解红黑树,我们需要先了解二叉查找树。
二叉查找树概述
二叉查找树(Binary Search Tree,简称BST)是一颗二叉树,它的左子节点的值比父子节点的值要小,右节点的值要比父节点的值要大。他的高度决定了它的查找效率。在理想情况下,二叉查找树增删改查的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)。
BST存在倾斜的问题:
1.平衡的BST:
2.倾斜的BST:(不平衡的BST树有可能退化成链表)
红黑树概述
红黑树(Red-Black Tree,简称RBTree)是一种自平衡二叉查找树,是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它的实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种系统语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。
RBTree也是函数式语言中最常用的持久数据结构之一,在计算几何中也有重要的作用。
红黑树的定义如下:
1.每个结点或是红的,或是黑的
2.根节点是黑的
3.每个叶节点是黑的
4.如果一个节点是红的,则它的两个儿子都是黑的
5.对于每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点。
RBTree在理论上还是一颗BST树,但是它在对BST的插入和删除操作时会维持树的平衡,既保证树的高度在【logN,logN+1】(理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。RBTree的查找操作就是BST的查找操作。
简单来说RBTree的插入操作的时间复杂度比BST的时间复杂度低,这就是JDK1.8中HashMap为什么选择红黑树的原因。
红黑树的插入规律
-
情况一:插入的新节点默认为红色,父节点是黑色的,不进行调整
- 情况二:插入的新节点默认为红色,父节点是红色:
- 叔叔节点是空的,旋转+变色(G(爷爷节点)+P(叔叔节点)变色)
- 叔叔节点是红色,父节点+叔叔节点变黑色,祖父节点变为红色
- 叔叔节点是黑色,旋转+变色
可以参考以下网址来观察红黑树的规律:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
红黑树的插入分析
static <K,V> HashMap.TreeNode<K,V> balanceInsertion(HashMap.TreeNode<K,V> root,
//新节点默认为红色 HashMap.TreeNode<K,V> x) {
x.red = true;
//xp表示x的父节点,xpp表示x的祖父节点,xppl表示x的左孩子节点,xppr表示xpp的有孩子节点
for (HashMap.TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果x没有父节点,则表示x是第一个节点,自动为根节点,根节点为黑色
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果父节点不是红色,或者x没有祖父节点,那么就说明x是第二层节点,父节点为根节点,直接返回根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//表示x的父节点为红色
//如果x的父节点是祖父节点的左孩子
if (xp == (xppl = xpp.left)) {
//祖父节点的右孩子,也就是x的叔叔节点不为空,且为红色
if ((xppr = xpp.right) != null && xppr.red) {
//父节点和叔叔节点由红色变为黑色
xppr.red = false;
xp.red = false;
xpp.red = true;
//将x替换为祖父节点然后进行递归
x = xpp;
}
//表示x的父节点是空的或者叔叔节点是黑色的
else {
//如果x在x父节点的右边,则进行左旋
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果x在x父节点的左边,则进行右旋
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
//如果x的父节点是祖父节点的右孩子
//表示x的父节点为黑色
else {
//如果x的左祖父节点不为空且为红色
if (xppl != null && xppl.red) {
//x的叔叔节点变黑,祖父节点变红
xppl.red = false;
xp.red = false;
xpp.red = true;
//将xpp作为新节点去递归
x = xpp;
}
如果x的左祖父节点为空
else {
//如果x插入到xp的左边,则进行右旋
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//右旋后进行左旋
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
下面是左旋代码实现,与右旋原理相同这里就不一一描述。
左旋场景:x作为xp的左节点,xp作为xpp的右节点。xpp -> xp -> x 在下面代码中我们看作 pp -> p ->r
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) {
//如果r存在左节点,那么将r的左节点作为p的右节点
if ((rl = p.right = r.left) != null)
rl.parent = p;
//如果r的祖父节点为空,那么将r节点变成黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
//如果r的祖父节点不为空,p是pp的左孩子节点,将pp指向r
else if (pp.left == p)
pp.left = r;
如果r的祖父节点不为空,p是pp的右孩子节点,将pp指向r
else
pp.right = r;
//将r指向p
r.left = p;
p.parent = r;
}
return root;
}
HashMap中的基本属性
// 默认容量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中的hash()方法
对比JDK1.7中的HashMap我们发现在JDK1.8中,根据key进行hash运算中的左移,异或等运算明显变少了。原因是因为在1.7中hash方法的目的是为了让hash值更加的散列,也就是让每个位桶上面的链表长度更短防止链表过长从而影响运算效率。而在1.8中因为链表改为红黑树,不仅解决了链表长度问题,也优化了查询效率。而且减少hash方法中的运算过程也节约我们硬件cpu的损耗。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap中的put()方法
不同点:1.对比JDK1.7我们发现在1.7中会创建一个Entry数组,而在1.8中用的是node数组,但是仅仅只是数组名称变了而已,里面的属性并没有发生改变。
不同点:2.对比JDK1.7我们发现在1.7中采用的是链表头插法,而在1.8中用的是链表尾插法。
过程:1.判断数组是否为空,为空则调用resize方法初始化一个数组。注意resize既包括初始化数组也包括数组的扩容。2.对key进行(h&table.length-1)运算得出key所在数组的下标位置,并判断此下标位置是否为空,如果为空则创建node对象。3.如果下标位置不为空,有三种情况:第一种情况如果put进来的key位置上存在node对象,那么就修改node对象对应的value值。第二种情况如果put进来的key位置上存在链表,首先判断链表长度是否大于8,大于8则将链表转为红黑树,小于8,则判断key位置是否存在冲突,是则进行值覆盖,否则插入到链表尾部。第三种情况如果put进来的key位置上存在红黑树,遍历树后把新的节点插入树中。我们在往红黑树插入新的节点的时候,红黑树的根节点会发生变化。4.modcount操作次数加一,后判断是否需要扩容最后返回null。
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;
//判断数组是否为空,为空则初始化一个数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过取余运算得到key的下标位置
//如果此位置是否为空,则创建node对象
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果此位置不为空
else {
Node<K,V> e; K k;
//判断put进来的key的下标位置是否是e的位置,如果是将p赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果key位置上存在红黑树。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果key位置上存在链表
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//如果put进来的key与链表不存在冲突,则插入到链表的尾部
p.next = newNode(hash, key, value, null);
//判断链表长度是否大于8,如果是将链表改为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断链表的key是否与put进来的key相等,如果是则跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//进行值覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断是否需要扩容,与1.7里面一样
if (++size > threshold)
resize();
afterNodeInsertion(evict);//这个方法没有用到,不用考虑
return null;
}
链表树化:
第一步:首先进行判断如果table为空
或者table.length小于64,则调用resize()方法进行扩容或初始化。然后遍历链表,将node单向链表转为TreeNode双向链表
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果table为空或者table.length小于64,则调用resize()方法。
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;
//遍历链表,将node单向链表转为TreeNode双向链表
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);
}
}
第二步:将TreeNode双向链表转为红黑树。大概思路就是将TreeNode头节点作为红黑树的根节点,然后依次将TreeNode的其他节点插入到红黑树中。 注意:生成完成后的红黑树的根节点不一定是原TreeNode链表的头节点也可能是其他的节点。
补充:假设我们的类中重写了HashCode方法想要返回原有的HashCode值可以用System.identityHashCode(testHashCode)得到。
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;
//通过hashCode值判断大小,得到要插入树的具体节点,后插入到树中
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);
}
第三步: 将生成的红黑树的根节点移到数组中table[index]位置中。注意:因为第二步:我们发现红黑树的根节点并不是TreeNode的头节点,下面代码一部分就是将红黑树根节点的的TreeNode节点,移动到链表的头部,作为头节点。
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
HashMap中的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;
}
如果是红黑树节点,则调用红黑树的查找目标节点方法 getTreeNode
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 1.首先找到红黑树的根节点;2.使用根节点调用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
使用根节点调用 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;
}
HashMap中的resize()方法
不同点:jdk1.7扩容条件是hashmap中存储的元素数大于阈值且发生hash碰撞。而jdk1.8中的扩容条件就是hashmap的存储的元素数大于阈值
不同点:在1.8中HashMap的resize方法既包含了初始化的功能又包含了扩容的功能。
不同点:在1.8扩容遍历链表的时候,会将链表拆成两个小链表,然后将这两个小链表分别插入新数组的index位置和index+oldTable.length位置。也就是说在进行链表转移的时候,1.7中的HashMap会遍历一个转移一个,而1.8中的HashMap会全部遍历后,在转移。
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;
}
红黑树节点,则进行红黑树的重 hash 分布
大概过程就是将TreeNode链表,拆成两个index位置和index+oldTable.length位置的TreeNode,我们将index位置的TreeNode称为为loTail,index+oldTable.length位置的TreeNode称为hiTail。那么我们分成三种情况考虑,如果拆分后的TreeNode的长度小于6,那么我们将该TreeNode转成Node链表。如果拆分后的TreeNode的长度大于6,那么则拆分后的将TreeNode的头节点存到对应位置。如果拆分后的两个TreeNode其中一个长度为0,那么我们直接转移原有TreeNode。
/**
* 扩容后,红黑树的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);
}
}
}
将红黑树节点转为链表节点, 当节点<=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;
}
以上是关于JDK1.8中的HashMap的主要内容,如果未能解决你的问题,请参考以下文章
Java中HashMap底层实现原理(JDK1.8)源码分析
Java中HashMap底层实现原理(JDK1.8)源码分析