解析HashMap(二)

解析HashMap(二)

一、概述

​ JDK1.8中对HashMap做了很大的优化,其中最重要的优化就是其底层实现将原来的“数组+链表”改为“数组+链表+红黑树”。下面通过源码来分析HashMap

二、源码分析:

1. 常量:

1
2
3
4
5
6
7
8
9
10
11
12
/* 默认的数组的长度 或者说 默认是buckets/bins的长度 16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/* 最大长度 2的30次方 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/* 默认的加载因子 用于计算阈值(threshold) */
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;

2. 属性:

​ 有些属性设置为transient是与序列化有关。

transient 关键字

Java序列化会把某一个类存储以文件形式存储在物理空间,但是以文件形式存储某些信息时,容易涉及到安全问题,因为数据位于Java运行环境之外, 不在Java安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。 应该在这些字段前加上transient关键字。它的意思是临时的,即不会随类一起序列化到本地,所以当还原后,这个关键字定义的变量也就不再存在。

1
2
3
4
5
6
7
8
9
10
11
12
/* 用于存节点的数组(节点会hash到table中的某一个index) */
transient Node<K, V>[] table;
/* 用于存HashMap中的所有节点 */
transient Set<Map.Entry<K, V>> entrySet;
/* 节点的总个数 */
transient int size;
/* 修改的次数 */
transient int modCount;
/* 决定是否 扩容 的阀值 */
int threshold;
/* 加载因子 用于计算阈值(threshold) */
final float loadFactor;

3. 内部类

Node<K, V>

​ 继承了Map.Entry<K, V>类。

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
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; // 节点的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;
}

/* 重写getKey()方法 */
@Override
public K getKey() {
return key;
}

/* 重写getValue()方法 */
@Override
public V getValue() {
return value;
}

/* 重写setValue()方法 替换原来的值,返回旧值*/
@Override
public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

/* 比较当前的节点与对象o是否属于同一个对象 final方法表示子类不能重写 */
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;
}
}

4. 构造函数:

​ 我发现构造函数中,并没有initialCapacity这个属性,仅仅使用其来判断容量异常,通过这个属性求threshold,之后没有使用它,数组的长度没有声明,所以如何给数组初始化多大长度呢?

​ 是通过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
/* 传入容量和负载因子两个参数 */
public HashMap(int initialCapacity, float loadFactor) {
//检查传入的容量大小是否有异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//如果大于最大容量,则直接赋值为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//检查loadFactor是否有异常,NaN指”不是一个数“
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor; //赋值loadFactor操作
this.threshold = tableSizeFor(initialCapacity); //根据初始容量计算出阀值
}

/* 传入容量一个参数 */
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR); //采用默认加载因子调用第一个构造函数
}

/* 不传参数 */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 其余的属性都采用默认值
}

tableSizeFor(int cap)

​ 这个方法和JDK1.7中的roundUpToPowerOf2()中使用的Integer.highestOneBit(int i)方法相似。

​ 返回值一定是2的幂,该方法是返回大于等于cap的最小2的幂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static final int tableSizeFor(int cap) {
int n = cap - 1; //为了处理本身就为2的幂
//step 1
n |= n >>> 1;
//step 2
n |= n >>> 2;
//step 3
n |= n >>> 4;
//step 4
n |= n >>> 8;
//step 5
n |= n >>> 16;
//现在得到了从最高位开始都为1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
此方法使用 移位 和 或运算 来实现的
首先我们需要知道
1 | X = 1, 0 | X = X
cap ≠ 2的幂
我们假设一个整数n的二进制为:1XXX XXXX (X表示可1可0)
step 1 : n = 1XXX XXXX | 01XX XXXX = 11XX XXXX
step 2 : n = 11XX XXXX | 0011 XXXX = 1111 XXXX
step 3 : n = 1111 XXXX | 0000 1111 = 1111 1111
step 4 : n = 1111 1111 | 0000 0000 = 1111 1111
step 5 : n = 1111 1111 | 0000 0000 = 1111 1111
step 6 : 1111 1111 + 1 = 1 0000 0000
我们知道这五步是将n从最高位开始全部置为1
第6步是得到大于等于n的最小2的幂

我们再来看看cap = 2的幂
cap = 1000
n = 0111
step 1 : n = 0111 | 0011 = 0111
step 2 : n = 0111 | 0001 = 0111
step 3 : n = 0111 | 0000 = 0111
step 4 : n = 0111 | 0000 = 0111
step 5 : n = 0111 | 0000 = 0111
step 6 : 0111 + 1 = 1000

5. 插入

put(K key, V value)

​ 我们发现put方法很简单,其核心方法是hash()putVal()

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

hash(Object key)

​ 该方法是返回keyhash值。主要是通过将keyhashcode的高16位和低16位进行异或处理,这样做的目的是,使得高位也参加hash的运算,使得哈希尽可能的均匀分布。

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

putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

hash:当前keyhash值;

onlyIfAbsent:为true时不改变原有的值,为falsevalue替代旧值;

evict:在HashMap的子类LinkedHashMap中会有用。

​ 数组的初始化在该方法中。

​ 使用hash & (length -1)来计算index,相当于取模运算,但是速度更快。

这是数组长度为2的幂的第一个用处,方便使用位运算来求index

​ 该方法,若该节点不存在,则将该节点插入,返回为null;否则,旧值替换新值,返回旧值。

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
/* 如果tab还没有创建数组的话,则需要去resize方法中创建数组 */
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 1. 首先根据hash值计算出该节点属于哪个bucket/bin,也就是index
* i = (n - 1) & hash 其实就等于 hash % n
* 2. 如果此时的bucket是空,表明这个bucket还没有任何节点存入,
* 因此生成新节点后直接放入到该bucket
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
/**
* 当前位置不为空,所以这个节点应该放到这个bucket的后面
* 有三种情况(互斥条件,要么1要么2要么3出现)
*/
Node<K, V> e; // 如果这个节点已经存入过,就会拿出那个节点并且赋给e
K k;
/*
* 情况1
* 此节点已经存在并且就在 bucket 的第一个位置,直接把 p 赋给 e
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
/*
* 情况2
* p是一个TreeNode节点,表明已经树化,因此要调用红黑树的插入或者调整等相关操作
*/
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
/*
* 情况3
* 不是当前位置也没有树化,则需要遍历链表
* 需要注意,如果是新增的节点,则链表中的数量就会增加,则时候要考虑是否会树化
*/
// 遍历链表并且记录链表个数
for (int binCount = 0;; ++binCount) {
// 从链表的第二个开始,因为p在第一种情况已经比较过了
if ((e = p.next) == null) {
//说明之前没有该节点
p.next = newNode(hash, key, value, null); // 插入到链表尾
/*
* 判断是否树化,需要用到TREEIFY_THRESHOLD
* 这里的减一,是因为是从第二个节点开始计数的
*/
if (binCount >= TREEIFY_THRESHOLD - 1)
//进行树化
treeifyBin(tab, hash);
break;
}
//表明链表中已经存在了这个节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
//当前节点不是,遍历下一个
p = e;
}
}

/* e不为null表明此节点已经存入过,所以这里没有modCount++
返回旧值,如果新值替换旧值
*/
if (e != null) {
V oldValue = e.value;
/* onlyIfAbsent为true时不改变原有的值,为false用value替代旧值;
如果oldValue为null,那onlyIfAbsent就不起作用了
*/
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于子类LinkedHashMap的方法
return oldValue;
}
}
/* 进行到这里,说明 e==null,则之前没有此节点(或者说key也行)存入过
*/
++modCount; // modCount++
if (++size > threshold) // 先增加size的个数 判断是否需要扩容
resize();
afterNodeInsertion(evict); // 用于子类LinkedHashMap的方法
return null;
}

6. 扩容

​ 扩容的条件:size > threshold,而初始值是在构造函数中通过this.threshold = tableSizeFor(initialCapacity);得到的。

​ 扩容有两种情况:

​ a) 进行数组的初始化,此时oldCap = 0。还能根据构造函数的不同分为两种情况:

​ 1) 使用带参的构造函数,有threshold,通过tableSizeFor(initialCapacity)得到。

​ 2) 使用不带参的构造函数,没有threshold,通过默认的容量大小*默认负载因子得到。

​ 当数组初始化完成后直接返回。

​ b) 数组被初始化过,数组被扩容为原来的两倍,此时oldCap > 0。数组扩容后,需要将原数组的元素移到新的数组上。

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
126
127
final Node<K,V>[] resize() {
// oldTab 是 table
Node<K,V>[] oldTab = table;
// oldCap 是 以前table的length,如果以前是null就为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr 是 旧阀值
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)
// 阀值变为原来的2倍
newThr = oldThr << 1;
} else if (oldThr > 0)
/**
* 第一次初始化 且 有阀值
* 有参的两个构造函数会进入这个分支
* 这个知道为什么没有initCapacity也可以给table初始化长度了
* 数组长度是通过“阀值”初始化
* newThr没有赋值
*/
newCap = oldThr;
else {
/**
* 第一次初始化 且 没有阀值
* 无参构造函数会进入这个分支
*/
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阀值为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"})
//初始化新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//设置一下新table
table = newTab;

/*
* 扩容后,判断是否需要移元素
* oldTab = null :如果第一次初始化的时候就直接返回了
* olfTab ≠ null :需要把所有在oldTab上的元素转移到新的table中
*/
if (oldTab != null) {
// 需要遍历每一个数组,还要遍历每个数组中的每一个节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
/*
* 表明这个bucket里面有节点
* 分为三个情况,和put中的三种情况相对应
*/
oldTab[j] = null;
if (e.next == null)
/* 情况一:只有一个节点
* 那直接rehash一下放到新的table中就可以了
*/
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/* 情况二:如果是树节点,需要调用红黑树的方法
*/
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
/* 情况三:是链表
* 直接遍历链表。
* 我们知道数组扩容为原来的2倍,
* 求index是的位运算从"hash & (length - 1)"变成"hash & (2*length - 1)"
* "2*length-1"就是最高位多了1,此时只需要看k.hash的最高位是1还是0
* 最高位为0,新位置=数组原来的位置
* 最高位为1,新位置=数组原来的位置+原数组的长度
* 这样就是将原链表分成两个链表
* 最后两个链表头放到table对应的bucket中
*/
// 结果为0的链表-loHead
Node<K,V> loHead = null, loTail = null;
// 结果为1的链表-hiHead
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 最高位为0的情况
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 最高位为1的情况
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
// rehash的结果没有改变
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// rehash的结果就在原有的hash的结果上加上oldCap就可以了
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

​ 扩容会伴随一次重新的hash分配,并且会遍历一遍原来哈希表中的所有元素,消耗大量资源,所以我们应该避免resize.

7. 获取

​ 获取元素要比插入元素简单,我们知道哈希表的最大特点就是不需要遍历可以直接找到元素所在的“桶位”。

​ 当我们找到元素的桶位时,也分3中情况:只有一个节点(或者第一个节点就是);是树节点;是链表。

get(Object key)

1
2
3
4
5
6
7
/**
* 通过key获取value
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode(int hash, Object key)

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
/**
* 通过hash找到元素的桶位
* 通过key判断元素是否存在
*/
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) {
//如果Node链表的第一个元素相等
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);
}
}
//找不到返回null
return null;
}

containsKey(Object key)

1
2
3
4
5
6
/**
* 判断是否包含指定key
*/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null; //返回node是否为null
}

containsValue(Object value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 判断是否包含指定value
*/
public boolean containsValue(Object value) {
Node<K,V>[] tab;
V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
//按照单链表的方式进行遍历,
//因为HashMap中 TreeNode 节点也存在next成员,可以用链表的方式进行遍历
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}

8. 删除

​ 删除有两种方式:

  1. remove(Object key)根据key删除
  2. remove(Object key, Object value) 根据keyvalue删除

需要注意:只有扩容操作,没有减少容量的操作。

remove(Object key, Object value)

1
2
3
4
5
6
7
8
9
10
/**
* 注意与remove(Object key)不同的两点:
* 1. 要根据key,value同时符合才可以删除该节点
* 2. 返回值是true 表明正确删除
* 返回值是false 表明没有这个节点
*/
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}

remove(Object key)

1
2
3
4
5
6
7
8
9
/**
* 根据key来remove在HashMap中的节点
* 如果删除成功,返回该节点
* 如果删除失败,返回null
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)

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
	/**
* @param hash hash值
* @param key 键
* @param value 值
* @param matchValue 如果true表明必须key和value同时符合要求才可以删除,如果false的话只需要key符合就可以
* @param movable 用于红黑树中的操作
* @return 如果存在这个节点,会返回此节点,否则返回null
*/
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;
/*
* 分两步:
* 1. 先寻找到节点,如果存在则放到node中
* 2. 如果存在删除该节点
*/

// 第一步 查找节点 类似于getNode()
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;
/**
* 如果存在这个节点,会把节点赋值给node
* 分三种情况:
*/
// 1. 就在hash值对应的bucket中是第一个元素
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 2. 在红黑树中
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
// 3. 在链表中
else {
do {
// 遍历链表查找,查找到后赋值给node
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
// 找到后退出循环,到达第二步
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
/**
* 第二步:删除操作
* 也分三种情况
* matchValue为false,后边是短路,针对只用key删除
*/
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 1. 树节点,调用红黑树的相关方法
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
// 2. 第一个节点,直接bucket节点设置为node.next
else if (node == p)
tab[index] = node.next;
// 3. 在链表中删除node节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); //留作子类LinkedHashMap中使用
return node;
}
}
return null;
}