目录
前言
日常开发经常或使用HashMap来存储对象、数据或者传递参数,知道它的原理key不可重复,key、value可为null,就可以使用了,但实际为什么会有这些特性,以及Hash冲突它是如何解决,一概不知,所以就花了点时间看了看源码。
这篇文章会从HashMap的创建、Put、索引定位、扩容、Hash冲突、链表转红黑树、Get、Remove这些方面,从源码获取答案。
准备工作
main方法简单写几行代码。
public static void main(String[] args) {
HashMap<String,Object> map = new HashMap<>();
map.put("第一个","zhanghao");
map.put("第二个","zhanghao2");
map.putAll(map);
map.remove("第一个");
}
HashMap创建
HashMap创建的时候,如果像我上面这种创建方式,是不会赋予初始容量的。在放入第一个元素时候长度会赋予上。
看下源码:(个人喜欢把loadFactor叫做阈值系数,因为:容量*阈值系数=阈值)
//1、传入容量和负载系数(我喜欢叫阈值系数,因为:容量*阈值系数=阈值)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//这里设置阈值
this.threshold = tableSizeFor(initialCapacity);
}
//2、传入容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//3、默认初始化,跟我创建方式一样
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//4、直接传入Map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
1、传入容量和阈值系数
容量就是这个map的初始化长度,阈值就是map中当达到阈值长度时就出发扩容,比如:长度为16,阈值系数为0.75,则map的阈值就是12,那么map内的key数量达到了12,那就开始扩容。
这个很简单,假如执行下面代码:
HashMap<String,Object> map = new HashMap<>(14,0.6f);
则,这个Map的初始容量就是4, 阈值系数为0.6,这里有个方法是tableSizeFor(initialCapacity)
//它会返回一个“容量”的根号倍
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这里的第一行则是取2的次方倍最近的数字,比如,传入13,则会传出15,看一下源码
@HotSpotIntrinsicCandidate
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;//这里可以理解为2的次方数字
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
//i = 13则会进入这个if判断,n次方减去2个次方,i无符号右移两位
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
//最后返回 29-1=28
return n - (i >>> 1);
}
最后int n = -1>>>28,-1右移28位变成15
tableSizeFor()方法最后返回16。
在这里初始化的阈值还不是真正的map阈值: 容量*阈值系数,真正的阈值是在放入第一个值时进行扩容时才会设置,再继续往后面看,原因写在“扩容”部分。
2、传入容量
这里调用初始方法1,只不过用的是初始阈值系数0.75,这里就不再细说了。
3、默认初始化
这里源码备注全部属性都是用默认的,也就是容量16,阈值系数0.75。
4、直接传入Map
这里使用的还是默认的阈值系数0.75,下一行调用了一个方法putMapEntries(),看一下源码
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();//获取map长度
if (s > 0) {
if (table == null) { // pre-size
//刚开始传入map,table一定为空,所以第一次会进来
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//这里的意思是,如果阈值设定后,长度还是大于阈值,那就直接-》扩容
else if (s > threshold)
resize();
//循环把map的key-value放入新的map中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
这里涉及到了,扩容,还有Put操作,后面再看。
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,如果是则扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2、通过hash值计算索引位置,称它为:索引定位,如果此处为空,则新增节点,这里可以看出HashMap的基础数据结构就是Node为元素的数组;(长度-1)和key的hash值做“&”操作可以定位,这里可以细看一下
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 判断p节点的key和hash值跟传入的key和hash是否相等,如果相等, 将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//3、判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法放置目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
//4、如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,因为第一次判断已经跳过了一个节点,所以这里减一
if (binCount >= TREEIFY_THRESHOLD - 1)
//5、转红黑树了,这里仔细看一下
treeifyBin(tab, hash);
break;
}
//如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
//如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点原来的value,并返回value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
//如果插入节点后节点数超过阈值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
这里按照代码块里面的顺序来看一下
1、扩容
从刚开始学HashMap就知道的,扩容是以原来长度2倍扩容,也就是2的次方,来看一下源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//老表的容量不为0,即老表不为空
if (oldCap > 0) {
//判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,因为这时阈值*2已经远远大于Integer.MAX_VALUE,所以直接赋值为最大就完了,并直接返回老表,
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//两倍扩容在这里:::::容量扩容两倍<最大容量并且容量>=16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//这里表示老表的容量为0, 老表的阈值大于0,
//这种情况是刚初始化完,第一次扩容才会出现的,是因为初始化时阈值设置为容量了,容量没有设置
//这里就把老阈值(初始化的容量)设置为新容量。
else if (oldThr > 0)
newCap = oldThr;
else {
//这里表示老表的容量为0, 老表的阈值为0,这种情况是我上面“准备工作”中的方式创建的map,将阈值和容量设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
//如果你使用 HashMap(int initialCapacity, float loadFactor)这种构造方式来初始化则就会进入这个if,这里才会设置真正的阈值,这就与上文对应上了
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新阈值设置为阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果老表不为空,则需遍历所有节点,将节点赋值给新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
//使用索引定位放置e节点
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果是普通的链表节点,则进行普通的重hash分布
Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> next;
do {
next = e.next;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
// 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
// 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新表
return newTab;
}
//如果你使用 HashMap(int initialCapacity, float loadFactor)这种构造方式来初始化则就会进入上面代码块的这个if判断,这里才会设置真正的阈值,这就与上文对应上了
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
2、索引定位
这里我平时一直记得是key是通过计算hash值然后在HashMap中存放,但是其实这只对了一半,具体原因看一下源码:
int index = (n - 1) & hash
这里的hash就是key的hash值,怎么计算的这里不细说了,那么这里使用这样的位运算就是为了减小hash冲突,具体原因如下:
1、Hash冲突
对于任意给定的对象,只要它的 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 冲突的概率。
3、放入树节点PutTressVal()
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;
//查找根节点, 索引位置的头节点并不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//将根节点赋值给p节点,开始进行查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//传入的h小于此节点hash值,这向左边遍历
if ((ph = p.hash) > h)
dir = -1;
//传入的h小于此节点hash值,这向右边遍历
//这里的逻辑源自于红黑树的特性,左边<根节点<右节点
else if (ph < h)
dir = 1;
//key和value都相等,则返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//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;
}
//否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
dir = tieBreakOrder(k, pk); // dir<0则代表k<pk,则向p左边查找;反之亦然
}
//---------------------------------------------------------------------
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
//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节点
//创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//调整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;
//2、进行红黑树的插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
1、find()
去看这个find方法,就会发现遍历过程与此处的树遍历类似,就不细说了,这里看懂了,那么find就能看懂,这里看不懂,那就多看几遍画虚线往上的部分!
2、moveRootToFront()
这个方法就是将root放到头节点的位置,如果当前索引位置的头节点不是root节点,,则将root的上一个节点和下一个节点进行关联, 将root放到头节点的位置, 原头节点放在root的next节点上,跟着这个思路看一下源码。
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];
//如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
if (root != first) {
Node<K,V> rn;
//将该索引位置的头节点赋值为root节点
tab[index] = root;
TreeNode<K,V> rp = root.prev; // root节点的上一个节点
//下面两个操作是移除root节点的过程
//如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
//如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
if (rp != null)
rp.next = rn;
//下面两个操作将first节点接到root节点后面
//如果原头节点不为空, 则将原头节点的prev属性设置为root节点
if (first != null)
first.prev = root;
//将root节点的next属性设置为原头节点
root.next = first;
//root此时已经被放到该位置的头节点位置,因此将prev属性设为空
root.prev = null;
}
//检查树是否正常
assert checkInvariants(root);
}
}
4、插入链表尾部
这里就是想强调一下,有个关键点,就是他这里判断了一下节点个数
if (binCount >= TREEIFY_THRESHOLD - 1)
这里TREEIFY_THRESHOLD = 8;
因为进入方法就已经判断一个头节点了,所以这里减一
5、转红黑树
这里直接进入方法看源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//1、容量小于64, 调用resize方法进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//继续索引定位,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将链表节点转红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//如果是第一次遍历,将头节点赋值给hd
if (tl == null) // tl为空代表为第一次循环
hd = p;
else {
//如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
p.prev = tl; // 当前节点的prev属性设为上一个节点
tl.next = p; // 上一个节点的next属性设置为当前节点
}
//将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
tl = p;
} while ((e = e.next) != null);
//将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
1、重新扩容
这里就引入了转红黑树的条件了,有时候在外面看只能看到节点数量大于8了,只看到了一半的条件,所以,转红黑树必须容量大于64节点数量大于8,才可以链表转红黑树哈!!
Get
Get方法就是从Map中取key对应的value,例如:map.get("key");
public V get(Object key) {
Node<K,V> e;
//调用getNode,又一次说明了,HashMap底层数据结构是node节点
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;
//判断容器不为空,并且头元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//看第一个值是不是需要的那个数据
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果是树节点,就调用getTreeNode方法,进去看其实还是find()方法,还是那句话,把PutTreeVal()方法看懂了,红黑树遍历啥的不是事儿了奥
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//普通节点那就直接暴力循环了
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
Remove
这个方法就是删除元素,例如::map.remove("key");
这个过程就是先找到这个元素,再删除。
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;
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//--------------------------------------------------------------------------
//从这往上和get方法差不多,基本一模一样,就是先找出这个节点,那就不说了,继续看下面
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//1、如果是树节点,则调用removeTreeNode()
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
1、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);
}
removeTreeNode 图解
本图解忽略红黑树的颜色,请注意。
下面的图解是代码中的最复杂的情况,即流程最长的那个,p 节点不为根节点,p 节点有左右节点,s 节点不为 pr 节点,s 节点有右节点。
另外,第一次调整和第二次调整的是本人根据代码而设定的,将第一次调整和第二次调整合起来看会更容易理解,如下:
- 第一次调整 + 第二次调整:将 p 节点和 s 节点进行了位置调换,选出要替换掉 p 节点的 replacement
- 第三次调整:将 replacement 节点覆盖掉 p 节点
2、untreeify()
在上面代码的第十点中,有个untreeify()方法,源码备注soo small,意思就是节点删除后节点个数小于6个,则会把红黑树又转为链表,源码就不看了,转红黑树会了这个自然没啥看头了。
总结
我从创建初始化、Put、扩容、索引定位、Hash冲突、链表转红黑树,Get、Remove几个方面来解析,现在来对每个段落做个小结:
初始化:
- 如果使用无参构造,则全部属性都为默认值,容量16,阈值12,阈值系数0.75;
- 使用有参构造,则容量会是你传入容量参数最接近的2的次方数,例如:传入14,容量就会变成16,但是阈值系数会一直是你传入的参数,阈值也是按照容量*阈值系数来计算;
这里还是强调一下,初始化完容量和阈值其实还没有赋上值,是在放入第一个元素之后才会定下来!!
扩容
- 如果达到阈值,容量则会按照不大于容量最大值的情况,以2倍方式进行扩容,阈值是一样的逻辑;
- 如果容量超过64,某个元素上的链表节点数超过8,则链表会转为红黑树;
- 扩容时会触发hash重新分布,在Put()方法中的源码看到了,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。
索引定位
索引定位就是按照(Capacity-1)&hash,取位运算来进行。
Hash冲突
这里HashMap采用了“索引定位”来减少Hash冲突,如果还是有则此节点后面添加节点变成链表的方式来解决Hash冲突。
Remove
这个方法中,强调一点就是节点个数删除后小于6了,就会触发红黑树转链表的操作。
最后,HashMap是非线程安全的,线程安全可以使用ConcurrentHashMap代替。
这套代码看下来,确实不是很简单,可以看出JDK团队的技巧高超!
文中部分图片(hash冲突部分图文、移除树节点部分图文代码)参考来自(史上最详细的 JDK 1.8 HashMap 源码解析_程序员囧辉的博客-CSDN博客),感谢大佬!
最后我还整理汇总了⼀些 Java ⾯试相关的⾼质量 PDF 资料和免费Idea账号
公众号:Java小白,回复“⾯试” 和“idea破解”即可获取!