(一)HashMap系列:负载因子0.75
(二)HashMap系列:树化阀值8,退化阀值6
(三)HashMap系列:2次方扩容
(四)HashMap系列:put元素(不看完将后悔一生!)
红黑树系列:
一、《算法—深入浅出》N叉树的介绍
二、《算法—深入浅出》红黑树的旋转
三、《算法—深入浅出》红黑树的插入
四、《算法—深入浅出》红黑树的删除
一、前言
建议在学习本节内容前,先把以上目录中的内容先了解一遍再来学习本篇,更加容易理解 HashMap 的思想。
二、HashMap延迟初始化
我们通常使用 HashMap 方式:
Map map = new HashMap();
HashMap默认构造函数:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
我们看到,构造方法中,只初始化了负载因子:0.75;并没有初始化数组(即分配内存)。
HashMap只有在存放第一个键值对时才初始化(put / putIfAbsent 都会调用 putVal):
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // table为null时,调用resize进行首次初始化
......
}
}
HashMap.resize:
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
......
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
......
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap]; // 创建新的对象数组
.....
return newTab;
}
}
三、添加对象
3.1、HashMap.put / putIfAbsent
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
// 直接存放,若存在则直接替换覆盖
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 如果存在则直接返回已存在的值,不存在则存入
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
}
3.2、HashMap.putVal
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算数组下标 => i,获取下标对应的对象 => p
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
代码看起来较多,较复杂,容我将其拆解,一一分析:
- 判断是否为首次初始化(上面延迟初始化一节已经分析,这里不再分析)
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
......
}
}
- hash后的下标对应的数组坑位为 null,则直接放入即可
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
// 计算数组下标 => i,获取下标对应的对象 => p
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
......
}
}
以下具体看注释:
- 情况A:hash后,数组对应的下标已经有元素,则判断是否为同一元素(同hash,key同内存地址,key同值);
- 情况B:红黑树查找(找到直接返回) / 插入节点(未找到);
- 情况C:单链表查找(找到还要判断情况A:是否为同一元素);未找到先插入到链表,再判断是否需要转为红黑树;
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
// 计算数组下标 => i,获取下标对应的对象 => p
if ((p = tab[i = (n - 1) & hash]) == null)
......
else {
Node e; K k;
// 情况A
// 相同的 hash、key,且值也相等
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 情况B(红黑树)
// 数组下标的节点是红黑树(而且还是树根)
// putTreeVal:
// 1. 先查找,若找到则返回该节点
// 2. 没找到(一定到了叶子 nil),插入节点并返回 null
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
// 情况C(单链表)
// 从链表头依次查找,若到链尾,则插入节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表插入节点后,需要判断是否达到树化的阀值(8 - 1 = 7)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break; // break时,e = p.next = null
}
// 若找到,需要判断(情况A)
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // break时,e = p.next != null
p = e; // p = p.next,继续循环
}
}
......
}
......
}
}
- 元素已经存在的处理情况
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
else {
......
// 到这里,表明元素要么已经存在 e 为旧值
// 要么 e 为null,表明插入到链表 或者 红黑树中
if (e != null) { // 待放入的元素已经存在
V oldValue = e.value; // 先获取旧值
if (!onlyIfAbsent || oldValue == null) // 若允许替换 或者 旧值为 null,则用新值替换(覆盖)
e.value = value;
afterNodeAccess(e); // LinkedHashMap 才会用到,HashMap 中该函数为空函数
return oldValue; // 若不许替换则返回旧值,否则返回新值
}
}
......
}
}
- 新增元素后还要判断是否需要扩容()
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
// 返回为 null 表明是新增元素;
// 返回有值,则表明之前就存在;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
// 到了这里,一定是新增了一个元素(无论是桶、单链表、还是红黑树),只要新增了元素,size就加1
// 判断数组新增了元素后,是否达到扩容阀值:负载因子 * Capacity
if (++size > threshold)
resize();
afterNodeInsertion(evict); // LinkedHashMap 才会用到,HashMap 中该函数为空函数
return null;
}
}
四、HashMap 极端测试(一定要看!)
看到最后一点(新增元素也要判断是否扩容,而不是 hash 数组占用率达到扩容阀值才考虑扩容),我以为我的源码有问题,因此,我写了一个测试用例来验证我的猜想(再次申明:我的源码是 JDK1.8)
先说说我的猜想:
- 前提:源码是无论新增在哪里(桶、链表、还是红黑树),都对 size + 1;
- 构造:我创造一个特殊类,它的 hash 值永远固定,但它的 equals 确不想等;这样,就能造成 hash 冲突;
- 结论:我先说结论吧,确实 resize 了,不过,出现了意外的惊喜;
构造特殊类:
public class SpecialClass {
@Override
public int hashCode() {
return 1; // 强制 hash 冲突
}
@Override
public boolean equals(Object obj) {
return false; // 强制两对象不相同
}
}
测试代码:
import java.lang.reflect.Method;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap map = new HashMap<>();
Method capacity = null;
try {
// 反射:每次新增元素后,查看是否 size+1 导致 resize
capacity = map.getClass().getDeclaredMethod("capacity");
capacity.setAccessible(true);
for (int i = 0; i < 13; i++) {
SpecialClass specialClass = new SpecialClass();
map.put(specialClass, specialClass);
System.out.println("[" + i + "] map.size = " + map.size() + ", capacity = " + capacity.invoke(map));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
实际输出结果:
[0] map.size = 1, capacity = 16
[1] map.size = 2, capacity = 16
[2] map.size = 3, capacity = 16
[3] map.size = 4, capacity = 16
[4] map.size = 5, capacity = 16
[5] map.size = 6, capacity = 16
[6] map.size = 7, capacity = 16
[7] map.size = 8, capacity = 16
[8] map.size = 9, capacity = 32
[9] map.size = 10, capacity = 64
[10] map.size = 11, capacity = 64
[11] map.size = 12, capacity = 64
[12] map.size = 13, capacity = 64
每次新增元素,size 确实是加 1,调试 HashMap 源码:
- [0] 放在桶中;
- [1] ~ [7] 放在单链表中;
- [8] 先插入链表,按照源码,应该转成红黑树,但是,我们在讲《HashMap系列:树化阀值8,退化阀值6》中说了,树化开启是需要 capacity = 64,然后,你发现打印出来的 log 竟然扩容了;
- [9] 又再次导致扩容;
- [10 ~ 12] 之后就不再扩容了;
想必大家已经想迫不及待的想听我的分析了,OK,这就GO!
上面的分析,问题肯定是出在单链表转红黑树这里,那么我们就直接看『树化』的代码:
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
// 计算数组下标 => i,获取下标对应的对象 => p
if ((p = tab[i = (n - 1) & hash]) == null)
......
else {
// 情况A
// 情况B(红黑树)
......
else {
// 情况C(单链表)
// 从链表头依次查找,若到链尾,则插入节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表插入节点后,需要判断是否达到树化的阀值(8 - 1 = 7)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 我们期望的答案就在这个树化方法里
treeifyBin(tab, hash);
break; // break时,e = p.next = null
}
......
}
}
......
}
......
}
}
单链表转红黑树(treeifyBin)
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
// 重点在这里:如果当前桶的大小,即 capacity < 64,则扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 树化过程
.....
}
}
然后,你就会发现一个奇怪的事情:
- 初始Capacity = 16:不断添加元素,直到单链表(达到阀值 8 - 1 = 7),申请转红黑树,结果红黑树直接让 HashMap 扩容 Capacity = 32,但!但是!扩容后,链表中的元素仍旧停留在统一的链表中,并没有高低位链表区分,因此,此时单链表长度是(8 - 1 = 7,不含桶中的第一个元素);
- 当前Capacity = 32:再添加一个冲突的元素,此时还是先链表逻辑,追加到表尾,然后申请转红黑树,结果不满足树化条件,因此,扩容 Capacity = 64,同理,所有元素仍在链表中,此时,链表长度是(9 - 1 = 8,不含桶中的第一个元素);
- 当前Capacity = 64:再添加一个冲突的元素,此时还是先链表逻辑,追加到表尾,然后申请转红黑树,此时满足树化条件,因此,链表转红黑树;
- 后面相同冲突的元素,都直接添加到红黑树中;
也许大家没注意,也许大家知道没说,我嘛,发现了,一定要分享给大家!
好啦,HashMap系列,到这里就全部结束了,大家如果有什么不清楚的,可以留言交流!