Java8 的 HashMap

数据结构

在 Java7 中 HashMap 底层主要利用数组、链表结构来实现,但是根据 HashMap 的存储结构,当某个 hash 位上的链表过长,查询性能就会退化,所以在 Java8 中,当链表的节点数达到 8 时会转化为红黑树,查询的时间复杂度为 $O(logn)$。

相关数据结构:数组链表红黑树

源码解析

在看 Java8 的源码之前,需要了解位运算:

  • <<:左移,低位补 0
  • >>: 带符号右移,原数是整数高位补 0,原数是负数高位补 1
  • >>>:无符号右移,无论原数是正数还是负数都在高位补 0
  • ^:异或,相同为 0,相异为 1
  • &:与,都为 1 结果为1,其他为 0

put 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

/**
* @param hash 插入 key 的 hash 值
* @param key 插入 key
* @param value 插入的值
* @param onlyIfAbsent 如果为 true,当 key 对应的 value 不存在时才插入
* @param evict 用于linkedHashMap的尾部操作
* @return 如果 key 对应的 value 存在,并且替换过则返回旧值
*/
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)
//如果数组为空,则进行数组初始化;初始化时没有指定大小则默认为 16
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//计算插入位置((n - 1) & hash),如果数组该位置还没有插入,则构建节点直接插入
tab[i] = newNode(hash, key, value, null);
else {
//数组该位置已经有节点插入,则需要该数组位置是以链表实现还是红黑树,选择不同的插入方法
Node<K,V> e; K k;
//判断数组该位置当前节点是否和要插入的节点 key 相等,如果相等取出当前节点,e 指向旧节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断数组该位置节点数据结构是是否是红黑树,如果是则调用红黑树的插值方法
e = ((TreeNode<K,V>)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);
//插入后如果该链表上的节点个数达到了 8 个,则把该链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//在链表遍历过程中,存在与待插入的节点相等的 key,结束循环,e 指向的就是旧节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果不为空,e 节点指向的是存在相等 key 的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果 onlyIfAbsent 为 true,当该节点对应的 value 不存在时才替换
//如果 onlyIfAbsent 为 false,直接用新节点 value 覆盖旧节点 value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果该 hashMap 中的所有节点个数超过阈值,则需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize 数组扩容分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
final Node<K,V>[] resize() {
//对 hash 数组扩容或者初始化
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//数组容量已经大于 0,判断是否超过最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//没有超过最大容量,直接扩大 2 倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//判断是否超过最大容量并且大于默认容量,则阈值扩大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//使用 HashMap(int initialCapacity) 初始化数组容量大小
//第一次执行 put 操作时,将数组容量初始化为计算的阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//使用 new HashMap() 初始化后,将数组容量初始化为默认值 16
newCap = DEFAULT_INITIAL_CAPACITY;
//阈值也是默认值 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//在 if (oldThr > 0) 这个条件中,第一次 put 时,newThr 值为 0
if (newThr == 0) {
//利用初始化的数组容量计算新的阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//用新的容量初始化新的 hash 数组
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;
if (e.next == null)
//旧数组该位置只有一个节点,计算该节点在新数组的索引值,插入节点
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果该节点是红黑树结构
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//每次扩容时,容量都是原来的 2倍,这种情况下每个元素在新数组中的索引位置:要么是不变,还有一种是原位置+扩容长度。
//当然也可以利用 e.hash & (newCap - 1)来计算,这两个是等价的。但是 java8 的这个处理性能好
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//根据上述的两种方式,可以将旧数组该位置量表分为两个链表,然后分别插入到新数组的对应位置
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

索引位置结算

hash & (cap - 1):利用模运算也可以计算索引,但是模运算的性能不如与运算。

例如:

cap 为 16 时,hash 值为 30:0001 1110 & 0000 1111 = 0000 1110,索引值为 14

cap 为 16 时,hash 值为 5:0001 1110 & 0000 0101 = 0000 0100,索引值为 4

在扩容时,链表复制的情况:

newCap 为 32,hash 值为 30 时:0001 1110 & 0001 1111 = 0001 1110 新的索引值是:30,符合旧的索引值 + 旧的数组大小

newCap 为 32,hash 值为 5 时:0001 1110 & 0000 0101= 0000 0100 新的索引值是:4,位置不变

get 函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
// 如果数组不为空,对应数组索引位置节点不为空时进行查找
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
//判断该位置首节点是不是要查找的节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果该位置上的节点是红黑树结构,执行红黑树的查找方法
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;
}