前言
HashMap是我们最常使用的数据类型之一,也是面试中最容易问到的集合之一。本篇文章主要通过分析HashMap的源码,更深入的了解HashMap,以及对比HashMap在JDK1.8和JDK1.7的不同。
本文基于jdk1.8
HashMap简介
HashMap是一个散列表,属于java.util
包,它存储的内容是键值对(key-value)映射 ,根据键的hashCode值存储数据。
HashMap类结构
可以从上图看出,HashMap继承AbstractMap抽象类,实现了Cloneable,Serializable和Map接口。
ps:为什么HashMap继承了AbstractMap抽象类,还要实现Map接口?
Java集合框架的编写者Josh Bloch说这是一个错误。。在java集合框架中,类似这样的写法很多。 JDK的维护者,后来不认为这个小小的失误值得去修改。所以就这样存在下来了。
HashMap特点
- HashMap非线程安全,在多线程中保证线程安全的解决方法:使用
ConcurrentHashMap
,Map map = Collections.synchronizedMap(new HashMap());
。 - HashMap底层是由数组和链表组成的,JDK1.8之后添加了红黑树,文章后续有介绍。
- HashMap的key和value都允许为null。
- HashMap的数据存储是无序的。
- 如果需要put的key为自定义的对象,需要重写该对象的equals方法和hashCode方法。注意如果对象是可变的,那你就有可能get不到你保存在HashMap中的数据了,所以重写hashCode的时候需要小心哦~
HashMap提供的API
方法名 | 方法解释 |
---|---|
clear() | 从HashMap中移除所有映射。 |
clone() | 返回HashMap实例的浅表副本:key和value本身未被克隆。 |
compute( K key, BiFunction <? super K,? super V,? extends V> remappingFunction ) | 根据key获取映射,如果存在将BiFunction的返回值设置为新的value,并返回新的value。如果获取不到映射,就设置新的映射。如果BiFunction的返回值为null,就删除该节点。 |
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | |
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | 根据key获取映射,如果存在将BiFunction的返回值设置为新的value,并返回新的value。如果获取不到映射,就返回null。如果BiFunction的返回值为null,就删除该节点。 |
containsKey(Object key) | 如果HashMap中包含指定key的映射,则返回true。 |
containsValue(Object value) | 如果HashMap中包含指定value的一个或者多个映射,则返回true。 |
entrySet() | 返回此HashMap中包含的映射的Set。 |
forEach(BiConsumer<? super K, ? super V> action) | 将给定操作(BiConsumer)应用于HashMap的每一个元素,直到处理完所有数据或操作抛出异常为止。 |
get(Object key) | 返回指定key映射到的值;如果HashMap中不包含key的映射,则返回null。 |
getOrDefault(Object key, V defaultValue) | 返回指定key映射到的值;如果HashMap中不包含key的映射,则返回defaultValue。 |
isEmpty() | 判断HashMap是否为空。 |
keySet() | 返回HashMap中包含的key的Set。 |
merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) | 功能大部分与compute相同,不同之处在于BiFunction中apply的参数,入参为oldValue、value,调用merge时根据两个value进行处理并返回value。 |
put(K key, V value) | 将key和value添加到HashMap。 |
putAll(Map<? extends K, ? extends V> m) | 将指定Map中的所有映射复制到此HashMap。 |
putIfAbsent(K key, V value) | 如果HashMap中key不存在,就保存key-value映射,并返回null;反之就返回已存在的value,并不覆盖为value。 |
remove(Object key) | 从HashMap中删除指定key的映射,并返回value。 |
remove(Object key, Object value) | 只有在指定key映射到指定value时,才删除该数据。 |
replace(K key, V value) | 替换key映射的value。 |
replace(K key, V oldValue, V newValue) | 只有在指定key映射到指定oldValue时,才替换成newValue。 |
replaceAll(BiFunction<? super K, ? super V, ? extends V> function) | 对HashMap调用给定函数(BiFunction)的结果替换每个数据的value,直到处理完所有数据或者该函数抛出异常。 |
size() | 获取HashMap中的映射数量。 |
values() | 返回HashMap中包含的value的集合。 |
HashMap源码
HashMap常量属性
1 | /** |
HashMap成员变量
1 | /** |
HashMap内部类
Node
1 | static class Node<K,V> implements Map.Entry<K,V> { |
TreeNode
TODO
HashMap构造函数
HashMap一共有四个构造函数:
1 | /** |
HashMap静态工具方法
hash(Object key)
1 | static final int hash(Object key) { |
tab[i = (n - 1) & hash])
这是定位数组下标位置的一小段代码,其中n是数组的长度,hash是调用hash(Object key)
方法返回的值。我们可以看到,这里使用数组长度减1然后与hash相与,获取数组下标。为什么要这么操作?
当n始终是2的幂次时,(n - 1) & hash
相当于hash % n
,即对hash取模。这也是hash算法中的除留余数法。
我们试一下数组长度为15和16,hashCode为4和5的情况下,计算数组下标的结果:
很明显,假设数组长度为15,hashCode为4和5的时候,会发生hash碰撞,会降低查询效率和空间利用率。
ps:这也是为什么HashMap容量必须是2的幂次的原因。
仔细想一下,如果hashCode二进制为0001 0001 0001 0001 0000 0000 0010 1011和0000 0000 0000 0000 0000 0000 0010 1011的情况时,进行(n - 1) & hash
,这样出来的下标是相同的。为了解决这个问题,HashMap使用hash(Object key)
方法,进行高位异或运算,使得只有相同hash值的两个值才会被放到数组中的同一个位置上形成链表。
我们可以看一下下面的图:
综上所述:HashMap的hash过程是:获取key的hashCode -> 高位异或运算 -> 取模运算。
tableSizeFor(int cap)
1 | static final int tableSizeFor(int cap) { |
代码里的描述可能有点抽象,不容易理解,让我们举个例子看一下:
假设我们传入的cap为33,然后tableSizeFor
方法的流程如下:
HashMap重要方法解析
put(K key, V value)
1 | public V put(K key, V value) { |
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
onlyIfAbsent
:如果为true,就不改变已存在的key对应的value,只有key不存在才会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
57final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组为空,进行扩容(设置数组tab和数组长度n)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 定位数组下标,判断该位置是否null,如果为null,直接插入新节点(设置节点p和数组下标i)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果定位到的节点hash和key与put的相同,后面会进行value覆盖(记录键k和节点e)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树节点,调用红黑树的putvalue方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果是链表节点
else {
// 遍历链表节点进行插入
for (int binCount = 0; ; ++binCount) {
// 如果定位到的节点的下一个节点为null,将该节点插入到定位到的节点之后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断是否达到转换为红黑树的长度,如果达到了就进行转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果定位到的节点hash和key与put的相同,后面会进行value覆盖(记录键k和节点e)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果相同key的节点存在,就覆盖value,并返回旧值,操作次数不需要加1
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null,进行覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// HashMap定义了该方法,LinkedHashMap有实现
afterNodeAccess(e);
return oldValue;
}
}
// 操作次数加1
++modCount;
// k-v映射数量加1,如果超过扩容阈值,就进行扩容
if (++size > threshold)
resize();
// HashMap定义了该方法,LinkedHashMap有实现
afterNodeInsertion(evict);
return null;
}resize()
1 | final Node<K,V>[] resize() { |
这样做的好处: TODO
treeifyBin(Node<K,V>[] tab, int hash)
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
26final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 判断数组是否为空,或者数组长度是否小于哈希桶(Node)转换为树(TreeNode)的最小容量,满足的话进行扩容,即还没达到转换为红黑树的大小
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 达到了转为红黑树的大小,计算需要转换为红黑树的链表的数组下标
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//新建一个树形节点,内容和当前链表节点e一致
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
//确定树头节点
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//让数组的第一个元素指向新建的红黑树头结点,之后这个桶里的元素就是红黑树了
if ((tab[index] = hd) != null)
// 塑造红黑树,前面的只是创建了一个二叉树,并没有设置颜色值
hd.treeify(tab);
}
}红黑树相关操作
TODO
put(K key, V value)
流程:
- 如果数组为空,进行第一次扩容。
- 计算key的hash
- 定位数组的下标:
hash & (length – 1)
。 - 如果该位置为null,插入新节点,操作计数,size+1,判断是否需要扩容,并返回null。
- 如果4不满足,判断该位置第一个节点的key是否与put的相符。
- 如果相符,就记录该节点。
- 如果不相符,判断该节点是否为红黑树。
- 如果该节点是红黑树,通过红黑树的方法进行put,记录返回节点。
- 如果该节点不是红黑树,就遍历列表,如果有key相符的节点,就记录该节点,如果没有就新增节点,并判断是否要转为红黑树。
- 判断记录节点是否为null
- 如果不为null,进行旧值覆盖,并返回旧值。
- 如果为null,操作计数,size+1,判断是否需要扩容,并返回null。
putIfAbsent(K key, V value)
1 | public V putIfAbsent(K key, V value) { |
onlyIfAbsent
:为true,就不改变已存在的key对应的value,只有key不存在才会put。
putAll(Map<? extends K, ? extends V> m)
1 | public void putAll(Map<? extends K, ? extends V> m) { |
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 如果HashMap数组为null,进行初始化扩容阈值
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);
}
// 如果传入的Map的size大于扩容阈值,就进行扩容
else if (s > threshold)
resize();
// 遍历传入的Map,调用putVal进行添加k-v映射
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(Map<? extends K, ? extends V> m)
也调用了putMapEntries
方法。
get(Object key)
1 | public V get(Object key) { |
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
25final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 数组不为空,定位到的数组下标对应的节点不为空(设置数组tab,设置数组长度n,设置定位到的第一个节点first)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断第一个节点与需要获取的key是否相同(包括hash值),是的话就返回第一个节点(设置k)
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 没满足上面的判断,并且第一个节点的下一个节点不为null
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;
}红黑树相关操作
TODO
get(Object key)
流程:
- 计算key的hash值。
- 定位数组的下标:
hash & (length – 1)
。 - 返回key对应的值。其中查找节点流程:
- 如果定位到的数组下标的第一个节点与key相符,就返回该节点。
- 如果1没满足,判断是否是红黑树,如果是,则根据红黑树查找节点的方法返回节点。
- 如果2没满足,遍历链表,返回与key相符的节点。
- 如果3没满足,返回null。
getOrDefault(Object key, V defaultValue)
1 | public V getOrDefault(Object key, V defaultValue) { |
remove(Object key)
1 | public V remove(Object key) { |
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
53final 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;
// 数组不为空,定位到的数组下标对应的节点不为空(设置数组tab,设置数组长度n,设置定位到的节点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节点与需要remove的节点key相符,就记录为node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// p节点与需要remove的节点key不相符,并且p的下一个节点不为null
else if ((e = p.next) != null) {
// 如果是红黑树,根据红黑树的获取节点方法,获取节点记录为node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 遍历链表,查找key相符的节点,并记录为node
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果记录的node不为null,并且matchValue为false或记录的node的value与传入的value相符,就进行remove
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果记录的node为红黑树,就调用红黑树的remove方法,移除节点,可能会触发红黑树转为链表的操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果记录的node是链表的第一个节点,将链表的第一个节点指向该节点的下一个节点
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
// 操作次数加1
++modCount;
// HashMap中的映射数减1
--size;
// HashMap定义了该方法,LinkedHashMap有实现
afterNodeRemoval(node);
// 返回移除的节点
return node;
}
}
// 数组为空,返回null
return null;
}红黑树相关操作
TODO
remove(Object key, Object value)
1 | public boolean remove(Object key, Object value) { |
replace(K key, V value)
1 | public V replace(K key, V value) { |
replace(K key, V oldValue, V newValue)
1 | public boolean replace(K key, V oldValue, V newValue) { |
compute相关方法和merge
TODO
HashMap引发死循环的情况
HashMap在JDK1.8和JDK1.7中的对比
总结
通过这篇文章,我们可以知道一下几个点:
- HashMap是线程不安全的,所以如果我们在多线程下,需要使用
Collections.synchronizedMap
或者ConCurrentHashMap
。 - HashMap在JDK1.8中进行了很多优化,具体可以看HashMap在JDK1.8和JDK1.7中的对比这一节。
- HashMap中链表转为红黑树,需要满足两个条件,一是链表长度>=8,二是数组长度>=64。
ps:有一些TODO的内容,后续有空了再进行添加。
欢迎关注博主其他的文章。