HashMap底层维护一个数组,数组中的每一项都是一个Entryjava
1
|
transient
Entry<K,V>[] table;
|
咱们向 HashMap 中所放置的对象其实是存储在该数组当中;数组
而Map中的key,value则以Entry的形式存放在数组中安全
1
2
3
4
5
|
static
class
Entry<K,V>
implements
Map.Entry<K,V> {
final
K key;
V value;
Entry<K,V> next;
int
hash;
|
而这个Entry应该放在数组的哪个位置上(这个位置一般称为位桶或者hash桶,即hash值相同的Entry会放在同一位置,用链表相连),是经过key的hashCode来计算的。app
1
2
3
4
5
6
7
|
final
int
hash(Object k) {
int
h =
0
;
h ^= k.hashCode();
h ^= (h >>>
20
) ^ (h >>>
12
);
return
h ^ (h >>>
7
) ^ (h >>>
4
);
}
|
经过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标:性能
1
2
3
|
static
int
indexFor(
int
h,
int
length) {
return
h & (length-
1
);
}
|
这个方法其实至关于对table.length取模。this
当两个key经过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。spa
当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,好比A和B都hash后都映射到下标i中,以前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,因此新值存放在数组中,旧值在新值的链表上).net
示意图:线程
因此当hash冲突不少时,HashMap退化成链表。code
总结一下map.put后的过程:
当向 HashMap 中 put 一对键值时,它会根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。
若是该位置没有对象存在,就将此对象直接放进数组当中;若是该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是不是否值相同,map不容许<key,value>键值对重复), 若是此链上有对象的话,再去使用 equals方法进行比较,若是对此链上的每一个对象的 equals 方法比较都为 false,则将该对象放到数组当中,而后将数组中该位置之前存在的那个对象连接到此对象的后面。
值得注意的是,当key为null时,都放到table[0]中
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private
V putForNullKey(V value) {
for
(Entry<K,V> e = table[
0
]; e !=
null
; e = e.next) {
if
(e.key ==
null
) {
V oldValue = e.value;
e.value = value;
e.recordAccess(
this
);
return
oldValue;
}
}
modCount++;
addEntry(
0
,
null
, value,
0
);
return
null
;
}
|
当size大于threshold时,会发生扩容。 threshold等于capacity*load factor
1
2
3
4
5
6
7
8
9
|
void
addEntry(
int
hash, K key, V value,
int
bucketIndex) {
if
((size >= threshold) && (
null
!= table[bucketIndex])) {
resize(
2
* table.length);
hash = (
null
!= key) ? hash(key) :
0
;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
|
jdk7中resize,只有当 size>=threshold而且 table中的那个槽中已经有Entry时,才会发生resize。即有可能虽然size>=threshold,可是必须等到每一个槽都至少有一个Entry时,才会扩容。还有注意每次resize都会扩大一倍容量
一直到JDK7为止,HashMap的结构都是这么简单,基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。
这样子的HashMap性能上就抱有必定疑问,若是说成百上千个节点在hash时发生碰撞,存储一个链表中,那么若是要查找其中一个节点,那就不可避免的花费O(N)的查找时间,这将是多么大的性能损失。这个问题终于在JDK8中获得了解决。再最坏的状况下,链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提升HashMap的效率。
JDK7中HashMap采用的是位桶+链表的方式,即咱们常说的散列链表的方式,而JDK8中采用的是位桶+链表/红黑树(有关红黑树请查看红黑树)的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。
JDK8中,当同一个hash值的节点数不小于8时,将再也不以单链表的形式存储了,会被调整成一颗红黑树(上图中null节点没画)。这就是JDK7与JDK8中HashMap实现的最大区别。
接下来,咱们来看下JDK8中HashMap的源码实现。
JDK中Entry的名字变成了Node,缘由是和红黑树的实现TreeNode相关联。
1
|
transient
Node<K,V>[] table;
|
当冲突节点数不小于8-1时,转换成红黑树。
1
|
static
final
int
TREEIFY_THRESHOLD =
8
;
|
以put方法在JDK8中有了很大的改变
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
|
public
V put(K key, V value) {
return
putVal(hash(key), key, value,
false
,
true
);
}
final
V putVal(
int
hash, K key, V value,
boolean
onlyIfAbsent,
boolean
evict) {
Node<K,V>[] tab;
Node<K,V> p;
int
n, i;
//若是当前map中无数据,执行resize方法。而且返回n
if
((tab = table) ==
null
|| (n = tab.length) ==
0
)
n = (tab = resize()).length;
//若是要插入的键值对要存放的这个位置恰好没有元素,那么把他封装成Node对象,放在这个位置上就完事了
if
((p = tab[i = (n -
1
) & hash]) ==
null
)
tab[i] = newNode(hash, key, value,
null
);
//不然的话,说明这上面有元素
else
{
Node<K,V> e; K k;
//若是这个元素的key与要插入的同样,那么就替换一下,也完事。
if
(p.hash == hash &&
((k = p.key) == key || (key !=
null
&& key.equals(k))))
e = p;
//1.若是当前节点是TreeNode类型的数据,执行putTreeVal方法
else
if
(p
instanceof
TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(
this
, tab, hash, key, value);
else
{
//仍是遍历这条链子上的数据,跟jdk7没什么区别
for
(
int
binCount =
0
; ; ++binCount) {
if
((e = p.next) ==
null
) {
p.next = newNode(hash, key, value,
null
);
//2.完成了操做后多作了一件事情,判断,而且可能执行treeifyBin方法
if
(binCount >= TREEIFY_THRESHOLD -
1
)
// -1 for 1st
treeifyBin(tab, hash);
break
;
}
if
(e.hash == hash &&
((k = e.key) == key || (key !=
null
&& key.equals(k))))
break
;
p = e;
}
}
if
(e !=
null
) {
// existing mapping for key
V oldValue = e.value;
if
(!onlyIfAbsent || oldValue ==
null
)
//true || --
e.value = value;
//3.
afterNodeAccess(e);
return
oldValue;
}
}
++modCount;
//判断阈值,决定是否扩容
if
(++size > threshold)
resize();
//4.
afterNodeInsertion(evict);
return
null
;
}
|
treeifyBin()就是将链表转换成红黑树。
以前的indefFor()方法消失 了,直接用(tab.length-1)&hash,因此看到这个,表明的就是数组的下角标。
1
2
3
4
|
static
final
int
hash(Object key) {
int
h;
return
(key ==
null
) ?
0
: (h = key.hashCode()) ^ (h >>>
16
);
}
|