avatar

目录
HashMap底层实现源码分析

HashMap底层实现原理

0.样例数据

java
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
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class CollectionTest {
public static void main(String[] args) {
//唯一的工作初始化负债因子(this.loadFactor = DEFAULT_LOAD_FACTOR)为0.75f
Map<String,Integer> map = new HashMap<>();
int count = 1;
//添加kv
for (char i = 65; i < 91; i++) {
map.put(String.valueOf(i),count);
count++;
}

//第一种遍历方式
Set<String> keySet = map.keySet();
Iterator<String> iterator = keySet.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println(key+ " => " + map.get(key));
}
System.out.println("******************************");

//第二种遍历方式
Iterator<Map.Entry<String, Integer>> iteratorMap = map.entrySet().iterator();
while (iteratorMap.hasNext()) {
Map.Entry<String, Integer> mapEntry = iteratorMap.next();
System.out.println(mapEntry);
}

System.out.println("******************************");
//第三种遍历方式
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " => " + entry.getValue());
}
}
}

1. 类信息

java
1
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

2. 基本属性

java
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
private static final long serialVersionUID = 362498820763181265L; //序列化版本号

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量16(左移4位相当于乘以2的4次方)

static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量(1073741824)

static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子

static final int TREEIFY_THRESHOLD = 8; //链表节点转换红黑树节点的阈值

static final int UNTREEIFY_THRESHOLD = 6; //红黑树节点转换链表节点的阈值

static final int MIN_TREEIFY_CAPACITY = 64;// 转红黑树时, table的最小长度

// 基本hash节点, 继承自Entry,此时的Node节点就是相当于Entry节点的实现
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}


transient Node<K,V>[] table; //hashMap数组的表示

transient Set<Map.Entry<K,V>> entrySet; //entry节点

transient int size; //数组长度

transient int modCount; //添加的元素个数

int threshold; //合理的初始化数组长度,根据tableSizeFor()得到,用于手动设置时使用

final float loadFactor; //负载因子,用于手动设置时使用

//构造器一:定义Node[]数组初始长度
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);
//为Node[]数组设置负债因子
this.loadFactor = loadFactor;
//为Node[]数组设置一个合理的值
this.threshold = tableSizeFor(initialCapacity);
}
//初始化Node[]数组长度,根据传入的值以2的n次方对数组进行扩容
//(例如:存入传入值为9,数组容量为16,在(8,16]范围内将不会再次扩容)。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= 1 << 30) ? 1 << 30 : n + 1;
}

//构造器二:调用HashMap(int initialCapacity, float loadFactor)构造器
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造器三:仅创建HashMap对象,并初始化负债因子为0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

// 红黑树节点
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
//...
}

3. hash算法

HashMap定位数组索引位置,直接决定了hash方法的离散性能。下面是定位哈希桶数组的源码:

java
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
// 计算key的hash值
static final int hash(Object key) {
int h;
// 1.先拿到key的hashCode值,基本数据类型会使用其包装类重载的hashCode()方法去计算hash值,引用数据类型根据是否重写去计算
// 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 将(tab.length - 1) 与 hash值进行&运算
int index = (tab.length - 1) & hash;
}

//对值进行Hash计算
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = valu
/**
* 当KEY值为A测试数据,A的hash为: 31 * hash + ANSI码值65
* 当KEY值为AB测试数据,AB的hash为:31 * 65 + 66
*/
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

HashMap底层数组的长度总是2的n次方,并且取模运算为“h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是HashMap在速度上的优化,因为&比%具有更高的效率。

在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table的length较小的时候,让高位也参与运算,并且不会有太大的开销。

4. get方法

java
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
//调用的GET方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//实际执行的GET方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
// table不为空 && table长度大于0 && table索引位置(根据hash值计算出)节点不为空
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// first的key等于传入的key则返回first对象
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
//first的key不等于传入的key则说明是链表,向下遍历
if ((e = first.next) != null) {
// 判断是否为TreeNode,是则为红黑树
// 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//走下列步骤表示是链表,循环至节点的key与传入的key值相等
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//找不到符合的返回空
return null;
}

5. put方法

java
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
//掉用的PUT方法,hash(key)调用本例中的hash()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

//实际执行的PUT方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;

// table是否为空或者length等于0, 如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个
if ((p = tab[i = (n - 1) & hash]) == null) // 将索引位置的头节点赋值给p
tab[i] = newNode(hash, key, value, null);

else { // table表该索引位置不为空
Node<K,V> e; K k;
//判断p节点的hash值和key值是否跟传入的hash值和key值相等
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果相等, 则p节点即为要查找的目标节点,赋值给e
// 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 走到这代表p节点为普通链表节点
else {
// 遍历此链表, binCount用于统计节点数
for (int binCount = 0; ; ++binCount) {
//p.next为空代表目标节点不存在
if ((e = p.next) == null) {
//新增一个节点插入链表尾部
p.next = newNode(hash, key, value, null);
//如果节点数目超过8个,调用treeifyBin方法将该链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
//map修改次数加1
++modCount;

//map节点数加1,如果超过阀值,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}

从上面的源码分析可以看出

1、如果节点已经存在,则更新原值

2、如果节点不存在,则插入数组中,如果数组已经有值,则判断是非是红黑树,如果是,则调用红黑树方法插入

3、如果插入的是链表,插入尾部,然后判断节点数是否超过8,如果超过,则转换为红黑树

4、先插入的数据,后面判断是否超过阀值再进行的扩容

putTreeVal,插入红黑树方法就不看了,看下treeifyBin方法,该方法是将链表转化为红黑树,

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
// table为空或者table的长度小于64, 进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 根据hash值计算索引值, 遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null); // 链表节点转红黑树节点
if (tl == null) // tl为空代表为第一次循环
hd = p; // 头结点
else {
p.prev = tl; // 当前节点的prev属性设为上一个节点
tl.next = p; // 上一个节点的next属性设置为当前节点
}
tl = p; // tl赋值为p, 在下一次循环中作为上一个节点
} while ((e = e.next) != null); // e指向下一个节点
// 将table该索引位置赋值为新转的TreeNode的头节点
if ((tab[index] = hd) != null)
hd.treeify(tab); // 以头结点为根结点, 构建红黑树
}
}

可以看到,会先判断tab的节点数是否超过64,如果没超过,则进行扩容,如果超过了才会转换为红黑树

可以得到两个结论

1、什么时候转换为红黑树

当链表数目超过8,并且map节点数量超过64,才会转换为红黑树

2、什么时候扩容(前提是map数目没有超过最大容量值 1<<30 )

新增节点时,发生了碰撞,并且节点数目超过阀值

新增节点时,发生了碰撞,节点数量木有超过阀值,但是链表数目>8,map节点<64时

再看下resize()方法

java
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
final Node<K,V>[] resize() {
//oldTab保存未扩容的tab
Node<K,V>[] oldTab = table;
//oldTab最大容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldTab阀值
int oldThr = threshold;
int newCap, newThr = 0;
//如果老map有值
if (oldCap > 0) {
// 老table的容量超过最大容量值,设置阈值为Integer.MAX_VALUE,返回老表
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
//老table的容量没有超过最大容量值,将新容量赋值为老容量*2,如果新容量<最大容量并且老容量>=16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值
newCap = oldThr; // 则将新表的容量设置为老表的阈值
else { //老表的容量为0, 老表的阈值为0, 则为空表,设置默认容量和阈值
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) { // 将索引值为j的老表头节点赋值给e
oldTab[j] = null; //将老表的节点设置为空, 以便垃圾收集器回收空间
// 如果e.next为空, 则代表老表的该位置只有1个节点,
// 通过hash值计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null) //
newTab[e.hash & (newCap - 1)] = e;
//e.next不为空,判断是否是红黑树
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;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:
// 老表的索引位置+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null; // 最后一个节点的next设为空
newTab[j] = loHead; // 将原索引位置的节点设置为对应的头结点
}
if (hiTail != null) {
hiTail.next = null; // 最后一个节点的next设为空
newTab[j + oldCap] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
}
}
}
}
}
return newTab;
}

可以看出,扩容时,节点重hash只分布在原索引位置与原索引+oldCap位置,为什么呢

假设老表的容量为16,即oldCap=16,则新表容量为16*2=32,假设节点1的hash值为0000 0000 0000 0000 0000 1111 0000 1010,节点2的hash值为0000 0000 0000 0000 0000 1111 0001 1010,则节点1和节点2在老表的索引位置计算如下图计算1,由于老表的长度限制,节点1和节点2的索引位置只取决于节点hash值的最后4位。再看计算2,计算2为新表的索引计算,可以知道如果两个节点在老表的索引位置相同,则新表的索引位置只取决于节点hash值倒数第5位的值,而此位置的值刚好为老表的容量值16,此时节点在新表的索引位置只有两种情况:原索引位置和原索引+oldCap位置(在此例中即为10和10+16=26)。由于结果只取决于节点hash值的倒数第5位,而此位置的值刚好为老表的容量值16,因此此时新表的索引位置的计算可以替换为计算3,直接使用节点的hash值与老表的容量16进行位于运算,如果结果为0则该节点在新表的索引位置为原索引位置,否则该节点在新表的索引位置为原索引+oldCap位置。

6. remove()方法

java
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
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;
// 如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
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;
// 如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
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) // 如果p是TreeNode则调用红黑树的方法查找节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do { // 遍历链表查找符合条件的节点
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 赋值给node, 并跳出循环
break;
}
p = e; // p节点赋值为本次结束的e
} while ((e = e.next) != null); // 指向像一个节点
}
}
// 如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果是TreeNode则调用红黑树的移除方法
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 走到这代表节点是普通链表节点
// 如果node是该索引位置的头结点则直接将该索引位置的值赋值为node的next节点
else if (node == p)
tab[index] = node.next;
// 否则将node的上一个节点的next属性设置为node的next节点,
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount; // 修改次数+1
--size; // table的总节点数-1
afterNodeRemoval(node); // 供LinkedHashMap使用
return node; // 返回被移除的节点
}
}
return null;
}

7. JDK1.7和1.8的区别

1、JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)

2、JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

3、扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1),而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

4、jdk1.7 先扩容再put ,jdk1.8 先put再扩容

文章作者: Yang4
文章链接: https://masteryang4.github.io/2020/03/16/HashMap%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MasterYangBlog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论