概览

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

从继承关系上看好像并不没有什么特点。它基于哈希表实现了Map接口,并且允许null的值和null的键。HashMap并不能保证有序性。并且它不是同步的。

[VJ08XQ.png]

属性分析

1
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

默认的初始容量为16.初始容量的大小和负载因子是非常影响其性能的两个参数。


1
static final int MAXIMUM_CAPACITY = 1 << 30;

最大的允许的容量


1
static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认的负载因子(装填因子)。这是一个非常重要的参数。负载因子的定义为:填入表中的元素的个数/哈希表的长度(桶的个数)。所以装填因子越大产生冲突的可能性也越大。简单的来说就是装的越多其装满程度也越大,其装填因子也就越大。


1
static final int TREEIFY_THRESHOLD = 8;

哈希表需要解决冲突问题,也就说不同的键可能hash到同一个桶中,必须要解决这个问题。jdk中采用的是链地址法。如果桶中元素的数大于这个值时,就会从链表转为红黑树。


1
static final int UNTREEIFY_THRESHOLD = 6;

当桶中元素的数量小于这个值的时候,就将红黑树转换为链表。


1
static final int MIN_TREEIFY_CAPACITY = 64;

桶中结构转化为红黑树对应的table的最小大小。


1
transient Node<K,V>[] table;

存取元素的数组,其大小总是2的n次幂。


1
transient Set<Map.Entry<K,V>> entrySet;

具体存放元素的集合。


1
transient int size;

存放的元素的个数。注意不是数组的长度。


1
transient int modCount;

用于记录hashmap结构改变次数。其用途和ArrayList中的一致。


1
int threshold;

临界值,当HashMap中的元素的个数(负载因子*容量)大于这个值,会进行扩容。


1
final float loadFactor;

填充因子。

重要方法分析

1.hash

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

HashMap最重要的当然时Hash方法啦。首先获取对象的hashCode()值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode做异或运算,返回结果。(其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,无论正数还是负数,都在高位插入0)

2.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
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
128
129
130
131
132
133
134
135
136
137
    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;
if ((tab = table) == null || (n = tab.length) == 0)
/*如果是新表则进行扩容操作*/
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
/*(n - 1) & hash确定元素应该放置的位置,如果该位置现在为空,则直接放入*/
tab[i] = newNode(hash, key, value, null);
else {
/*该位置已经有元素了,也就是说现在要进行冲突处理了*/
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; /*如果还没有添加过元素则oldCap=0*/
int oldThr = threshold; /*保存旧的临界值*/
int newCap, newThr = 0;
if (oldCap > 0) {
/*如果不是还没添加过元素的hash表*/
/*对刚创建还没添加元素的Hash表确定容量和临界值*/
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
/*如果这个时候旧的临界值还是缺省值就将新的容量定为默认的初始容量即16;将新的临界值定位负载因子*默认容量大小*/
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) {
/*如果不是新表,也就说现在表中已经有元素了,现在才开始真正的扩容操作*/
/*复制元素,重新进行hash,这一步非常的耗时*/
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;
}

putVal的执行流程如图:
[VGb6OJ.md.png]

3.get方法

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
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}


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) {
/*确保Hash表中有数据,否者直接返回null*/
if (first.hash == hash && // always check first node
((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);
}
}
return null;
}

关于扩容的思考

没添加一个元素进入哈希表中size就会加一,当size的值大于threshold。就会进行扩容。且扩容的成本很高。当时如果之前插入的元素都在同一个桶,那么扩容其实意义不大。哈希碰撞攻击也是利用了这个漏洞。