HashMap

HashMap是是经典的数据结构, 也是Java最常用的数据结构之一. 由于HashMap不涉及多线程问题, 且作者 因此代码理解难度比较低, 非常值得一读.

HashMap的源码真的有一种作者希望我能看懂的感觉

  • 如果多个Key具有同样的Hash值, 但Key本身是Comparable的, 那么将通过比较的方式减少Key冲突造成的

计算哈希值

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

将对象的哈希值与高16位做异或操作, 从而保证高位的bit能够影响到低位的bit. 由于HashMap只根据当前table的大小是否部分的低位bit,因此这一操作有助于提高Hash函数的不一致性, 减少冲突.

注意在表达式中赋值的用法, 为了简化代码, HashMap中大量使用了此技巧

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

构造函数中是一些检查操作. 注意, 此时并没有分配任何的内存, 因为HashMap采取了惰性加载机制, 只有真的开始使用HashMap的时候才会分配内存.

添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
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();
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);
}
}
}

当实际向HashMap中添加数据后, 进入此方法. 此方法实际上包含两个逻辑分支, 即如果还没有初始化(table == null)或者需要扩容(s > threshold)则调用对应的方法进行初始化或者扩容. 否则正常的添加数据.

扩容操作

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
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;

// 此部分计算容量, 按照出现概率, 最容易出现的情况最先判断
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
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
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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"})
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
// 链表节点分裂成两个链表
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;
}

第一段if-else语句重新计算了容量. 这一段是按照出现概率写的, 即普通的扩容操作最常见,因此在第一个if中处理, 而初始化操作只会执行一次, 因此最后处理.

扩容操作很简单, 根据节点类型进行分裂即可. 由于HashMap的大小始终是2的幂, 因此最需要按照哈希值的最高位重新分布即可.

参考资料

以下的几篇参考资料中, 第一篇重点分析了HashMap的插入过程, 解释了其中使用的一些二进制技巧, 并且提供了插入过程的流程图. 第二篇重点介绍了插入过程中, 红黑树的操作, 从红黑树的基本操作开始, 详细介绍了HashMap的红黑树创建和插入过程.

第三篇完整的介绍了HashMap的插入, 删除过程, 对源代码的注解比较详细.第四篇文章在对源代码进行注释的同时, 还提供了操作逻辑的总结, 看文字总结更容易理解.

第五篇介绍了JDK1.7中为什么HashMap会产生死循环.

以下几篇参考资料中, 第一篇介绍了红黑树的插入和删除过程, 不过全程使用伪代码描述, 直接看可能看不懂.

JDK类库源码分析

多线程环境DEBUG

可以对所有线程在指定的位置进行断点, 所以即使是多线程环境, 也能够一步步的执行代码

最后更新: 2024年04月18日 13:26

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2021/09/09/Java%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B%E9%9B%86%E5%90%88%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/