HashMap源码分析
0、绪论
hashMap是一个很重要的数据结构首次出现在JDK1.2中 。简单的来说就是将数据的hash码按照相近的程度分别装入一些桶。每一个桶中的对象的hash值相似。这样可以按照需要查找的对象的hash值,来快速逼近所需要查询的对象的本体。
1、初始容量
初始容量要求是2的n次幂:即为16,32,64,128 etc
源码中:1 >> 4 = 2^4 = 16 ,所以缺省的初始容量是16
2、负载因子
负载因子表示扩展的比例,已经存入的KV对的数量占capacity的最大比例。
0.75 这个缺省的负载因子,在时间成本和空间成本上做到了最好的折衷。过大省空间但是浪费了时间,过小省时间但是浪费了空间。
3、阈值
阈值:threshold = initcapacity * loadFactory
阈值表示这个hashMap所能容纳的最大的KV对数。(一个K和一个对应的V叫做一个键)超过了这个数量,容量就需要翻倍,即 capaciry >> 1。
为2的n次幂的原因是:对每一个桶进行重新排列的时候,hash码是二进制的形式,每一位上只有0 和 1 ,当容量增大的时候,对应的位数上为0 的保持在原来的位置 n 上,为1 的变到 n + oldLength 上
1、原理
构建一个数组,这个数组成为桶数组(bucket-array)。这个数组中存放的是一个链表的头节点或者是一个红黑树的根结点。
属于桶数组的数据结构是链表还是红黑树,要取决于属于这个桶KV对的数量。
如果在8个以下,那么这个数据结构一定会是一个链表。
如果链表长度到达了8个,那么有两种情况
1、桶数组的长度在64以下,那么就会进行扩容操作,将桶的数量翻倍。
2、桶数组的长度到达了64,就会将这个链表变成一个红黑树。
如果某个桶中装的红黑树,经过删除操作之后又回到了7个,就会降级,变成一个链表。
以上的操作,解决了hash冲突。
还有一种导致扩容的条件:
那就是table中的 容量 * 负载因子 都填充了对象,那么就会导致扩容。这就是构造方法里面的初始设置的变量的意义。
2、HashMap的构造方法
HashMap 有四个构造方法:
0、无参构造方法:
1 public HashMap() {
2
3 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
4
5 }
1、初始桶数量构造方法:
1 public HashMap(int initialCapacity) {
2
3 this(initialCapacity, DEFAULT_LOAD_FACTOR);
4
5 }
2、初始桶数量和负载因子构造方法:
1 public HashMap(int initialCapacity, float loadFactor) {
2
3 if (initialCapacity < 0)
4
5 throw new IllegalArgumentException("Illegal initial capacity: " +
6
7 initialCapacity);
8
9 if (initialCapacity > MAXIMUM_CAPACITY)
10
11 initialCapacity = MAXIMUM_CAPACITY;
12
13 if (loadFactor <= 0 || Float.isNaN(loadFactor))
14
15 throw new IllegalArgumentException("Illegal load factor: " +
16
17 loadFactor);
18
19 this.loadFactor = loadFactor;
20
21 this.threshold = tableSizeFor(initialCapacity);
22
23 }
3、从map导入生成构造方法
1 public HashMap(Map<? extends K, ? extends V> m) {
2
3 this.loadFactor = DEFAULT_LOAD_FACTOR;
4
5 putMapEntries(m, false);
6
7 }
方法0 :
以无参的方式构造一个hashmap,使用缺省的桶的数量16,使用缺省的负载因子0.75。
方法1:
自行设置初始的桶的数量。并且这个值只能为2的n次方。
方法2:
自行设置初始的桶的数量,自行设置初始的桶负载因子(0~1.0f)。
方法3:
通过一个map对象,将这个map对象中的所有KV对全部倒入这个hashmap中,这个hashMap使用缺省的初始桶数量和缺省的负载因子。
3、HashMap的查找
使用get方法和getNode方法实现查找。
1、get方法。
1 public V get(Object key) {
2
3 Node<K,V> e;
4
5 return (e = getNode(hash(key), key)) == null ? null : e.value;
6
7 }
在get方法里面包含着一个getNode方法,get方法返回的是getNode方法返回值的一个V。
2、getNode方法。
1 final Node<K,V> getNode(int hash, Object key) {
2
3 Node<K,V>[] tab;
4
5 Node<K,V> first, e;
6
7 int n;
8
9 K k;
10
11 if ((tab = table) != null && (n = tab.length) > 0 &&
12
13 (first = tab[(n - 1) & hash]) != null) {
14
15
16
17 if (first.hash == hash && // always check first node
18
19 ((k = first.key) == key || (key != null && key.equals(k))))
20
21 return first;
22
23
24
25 if ((e = first.next) != null) {
26
27 if (first instanceof TreeNode)
28
29 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
30
31 do {
32
33 if (e.hash == hash &&
34
35 ((k = e.key) == key || (key != null && key.equals(k))))
36
37 return e;
38
39 } while ((e = e.next) != null);
40
41 }
42
43
44
45
46
47 }
48
49 return null;
50
51 }
getNode方法是hashmap主要的查找方法。
1、桶数组table(tab)不能为空,桶的长度(n)不能为0,key对应的桶(first)不能为空。
2、如果桶的hash和传入的hash相同,并且key对应的桶的key和传入的key时第一个key是同一个key,那么这个时候,桶中的root或者head节点就是我们需哦需要的结果,返回它即可。
3、在第二个不成立的基础上,那么就进入循环。
4、在循环的第一次,判断是不是一个二叉红黑树,如果是,那么就直接调用树的查询方法,并返回这个结果。
5、如果不是一个树,那么就要通过一个循环来完成这个,查询的结果。
6、在链表中判断的结果和第二条的要求一致。
3、HashMap的遍历
有两种便利方法:
1、K (key) 遍历
可以使用foreach方法:
1 for (Object key : map.keySet()) {
2
3 System.out.println((String) key);
4
5 }
map.keySet的作用是返回一个map的Set对象,这样的情况,对hashMap的遍历,就转化成了对这个Set对象的遍历
中keySet的源码:
1 public Set<K> keySet() {
2
3 Set<K> ks = keySet;
4
5 if (ks == null) {
6
7 ks = new KeySet();
8
9 keySet = ks;
10
11 }
12
13 return ks;
14
15 }
1、如果不为空,直接的返回内部用来存放key的容器对象 :keySet(来自于抽象类AbstractMap)。
2、如果为空,new 一个空keySet,将其返回。
2、Entry 遍历
1 for (HashMap.Entry entry : map.entrySet()) {
2
3 System.out.print(entry.getKey()+" -> "+entry.getValue() + "
");
4
5 }
1、使用了EntrySet方法,返回一个用来存放Entry的容器EntrySet(来自于HashMap)。
2、Entry是和K、V操作相关的一个接口。
#、K、V 存放在Entry中
#、Entry的集合是EntrySet
3、获取entrySet,对其遍历,可以获得所有的Entry。
4、提取每一个Entry的K、V,可以获得所有的键对。
其中entrySet的源码:
1 public Set<Map.Entry<K,V>> entrySet() {
2
3 Set<Map.Entry<K,V>> es;
4
5 return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
6
7 }
1、如果不为空,直接的返回内部key和value的容器 :EntrySet。
2、为空,new 一个空EntrySet,将其返回。
4、HashMap的添加
1、添加方法
1 public V put(K key, V value) {
2
3 //这里的hash方法在这段代码的最后,不同于一般的hashcode。
4
5 return putVal(hash(key), key, value, false, true);
6
7 }
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
2
3 Node<K,V>[] tab;
4
5 Node<K,V> p; //p是桶中的第一个KV对
6
7 int n, i; //n是table的长度,i是桶数组的index。
8
9
10
11 //如果在hashmap中table是空的,就创建一个。
12
13 if ((tab = table) == null || (n = tab.length) == 0)
14
15 n = (tab = resize()).length;
16
17
18
19 //如果这个桶是空的,那么在这个桶中创建第一个节点。
20
21 if ((p = tab[i = (n - 1) & hash]) == null)
22
23 tab[i] = newNode(hash, key, value, null);
24
25
26
27 else {
28
29 //要插入的KV对
30
31 Node<K,V> e;
32
33 K k;
34
35
36
37 //如果桶中的第一个KV对的key的hash和要插入的hash相同,那么就把它替代掉
38
39 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
40
41 e = p;
42
43
44
45 //如果桶中的第一个节点下面挂的是一棵树
46
47 else if (p instanceof TreeNode)
48
49 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
50
51
52
53 //如果桶中的第一个节点装的是一个链表
54
55 else {
56
57 //查出尾节点,然后挂上节点就ok了
58
59 for (int binCount = 0; ; ++binCount) {
60
61 if ((e = p.next) == null) {
62
63 p.next = newNode(hash, key, value, null);
64
65 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
66
67 treeifyBin(tab, hash);
68
69 break;
70
71 }
72
73 if (e.hash == hash &&
74
75 ((k = e.key) == key || (key != null && key.equals(k))))
76
77 break;
78
79 p = e;
80
81 }
82
83 }
84
85
86
87 //复符合了上面的一个,然后返回V
88
89 if (e != null) { // existing mapping for key
90
91 V oldValue = e.value;
92
93 if (!onlyIfAbsent || oldValue == null)
94
95 e.value = value;
96
97 afterNodeAccess(e);
98
99 return oldValue;
100
101 }
102
103 }
104
105 ++modCount;
106
107 if (++size > threshold)
108
109 resize();
110
111 afterNodeInsertion(evict);
112
113 return null;
114
115 }
1、添加方法实际上是在内部调用了一个更加详细的添加方法,真正的添加的操作都是在这了详细的操作里面完成的。
2、并且这里所使用的hash方法不是直接的hashCode。
2、hash()方法
1 static final int hash(Object key) {
2
3 int h;
4
5 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
6
7 }
1、>>> :忽略符号位右移
2、<<< :忽略符号位左移
3、A^B :以二进制的形式按位取反
4、这样重新计算一次hash的值,增加了它的复杂度。让它们(KV键对)可以更好的在桶数组上均匀分布。
5、HashMap的resize(扩容)
resize方法在权衡之后,判断是否需要对node的数组table的长度翻倍处理。如果需要,将原来的元素分配到新的数组里。table的长度必须为2的幂。(16、32、64、128、256 etc.)
1 final Node<K,V>[] resize() {
2
3 Node<K,V>[] oldTab = table;
4
5 int oldCap = (oldTab == null) ? 0 : oldTab.length;
6
7 int oldThr = threshold;
8
9 int newCap, newThr = 0;
10
11
12
13 //说明初始化过了,将新的容量赋给newCap
14
15 if (oldCap > 0) {
16
17 if (oldCap >= MAXIMUM_CAPACITY) {
18
19 threshold = Integer.MAX_VALUE;
20
21 return oldTab;
22
23 }
24
25 //容量翻倍
26
27 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
28
29 oldCap >= DEFAULT_INITIAL_CAPACITY)
30
31 newThr = oldThr << 1; // 阈值翻倍
32
33 }
34
35 //下面的2个条件分支都是说明未被初始化
36
37 else if (oldThr > 0) // initial capacity was placed in threshold
38
39 newCap = oldThr;
40
41 else { // zero initial threshold signifies using defaults
42
43 newCap = DEFAULT_INITIAL_CAPACITY;
44
45 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
46
47 }
48
49
50
51 //在上面的选项里面执行的是未初始化过的那第二个条件分支,那么这个判断就会为真,然后将newThr赋值
52
53 if (newThr == 0) {
54
55 float ft = (float)newCap * loadFactor;
56
57 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
58
59 (int)ft : Integer.MAX_VALUE);
60
61 }
62
63
64
65 threshold = newThr;
66
67
68
69 @SuppressWarnings({"rawtypes","unchecked"})
70
71 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
72
73 table = newTab;
74
75
76
77 //利用新的容量和阈值,构建新的table,并将oldTable转移到新的Table中
78
79 if (oldTab != null) {
80
81 //遍历oldTable
82
83 for (int j = 0; j < oldCap; ++j) {
84
85 //oldTable中的每一个桶的第一个节点
86
87 Node<K,V> e;
88
89 if ((e = oldTab[j]) != null) {
90
91 oldTab[j] = null;
92
93 //单节点
94
95 if (e.next == null)
96
97 newTab[e.hash & (newCap - 1)] = e;
98
99 //下挂树
100
101 else if (e instanceof TreeNode)
102
103 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
104
105 //下挂链表
106
107 else { // preserve order
108
109 Node<K,V> loHead = null, loTail = null;
110
111 Node<K,V> hiHead = null, hiTail = null ;
112
113 Node<K,V> next;
114
115 do {
116
117 next = e.next;
118
119 if ((e.hash & oldCap) == 0) {
120
121 if (loTail == null)
122
123 loHead = e;
124
125 else
126
127 loTail.next = e;
128
129 loTail = e;
130
131 }
132
133 else {
134
135 if (hiTail == null)
136
137 hiHead = e;
138
139 else
140
141 hiTail.next = e;
142
143 hiTail = e;
144
145 }
146
147 } while ((e = next) != null);
148
149 if (loTail != null) {
150
151 loTail.next = null;
152
153 newTab[j] = loHead;
154
155 }
156
157 if (hiTail != null) {
158
159 hiTail.next = null;
160
161 newTab[j + oldCap] = hiHead;
162
163 }
164
165 }
166
167 }
168
169 }
170
171 }
172
173 return newTab;
174
175 }
6、链表的树化和树的链化
1、树化的几个阈值
1 /**
2
3 * 树化必须要达到的节点数
4
5 */
6
7 static final int TREEIFY_THRESHOLD = 8;
8
9
10
11 /**
12
13 * 取消树化的节点数
14
15 */
16
17 static final int UNTREEIFY_THRESHOLD = 6;
18
19
20
21 /**
22
23 * 树化的最小table长度。(桶数量)
24
25 低于这个数量说先进行table扩容
26
27 */
28
29 static final int MIN_TREEIFY_CAPACITY = 64;
这几个常量规定了hashMap中用来存储节点的table数组中存储的节点形态变化的阈值。
具体可以为以下几点:
1、树化的条件有两点:
(1)这个桶中存储的节点数量已经达到了8个
(2)桶的数量(table的长度)已经到达了64
满足这个两个条件,桶中的链表就会变成一个红黑树。
2、树化之后,当树的的节点已经不足6个了,那么就会取消树化返回链表的形态。
树是一个红黑树。
7、HashMap的删除
1 /**
2
3 * 删除这个KV对,如果K在hashMap中不存在,或输入的KV不对应,就返回false。
4
5 * 如果KV对应,那么删除他们,返回true;如果不对应,返回false,其他的什么都不做。
6
7 * 如果存在,删除这个KV对,并且返回true。
8
9 */
10
11
12
13 public boolean remove(Object key, Object value) {
14
15 return removeNode(hash(key), key, value, true, true) != null;
16
17 }
18
19
20
21
22
23 /**
24
25 * 删除这个K对应的V的值,并且返回这个V值,如果这个K不存在,那么返回null。
26
27 * 这个方法和pop有些类似,删除了之后,返回一个这个对应的V值。
28
29 */
30
31 public V remove(Object key) {
32
33 Node<K,V> e;
34
35 return ( e = removeNode( hash(key), key, null, false, true) ) == null ? null : e.value;
36
37 }
38
39
40
41 /**
42
43 * 方法为final,不可被重写。
44
45 * 上面的两个删除的方法都是调用这个方法实现删除的。
46
47 *
48
49 * @param hash key的hash值,该值是通过hash(key)获取到的
50
51 * @param key 要删除的键值对的key
52
53 * @param value 输入的V是否有效取决于 matchValue 是否为真
54
55 * @param matchValue 是否要求输入的KV对应,如果该值为真,但是KV不对应,就不删除。如果为假,则不关心输入的V的值
56
57 * @param movable 删除后是否移动节点,如果为false,则不移动
58
59 * @return 返回被删除的节点对象,如果没有删除任何节点则返回null
60
61 */
1 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
2
3 Node<K,V>[] tab; //tab : table
4
5 Node<K,V> p; //p: 对应的桶中的第一个节点
6
7 int n, index; //n:桶数组长度;index:对应的桶的index
8
9
10
11 //桶数组不为空、对应的桶也不为空。
12
13 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
14
15 Node<K,V> node = null, e;
16
17 K k;
18
19 V v;
20
21
22
23 //刚好是桶中的第一个节点,将 p 赋给 node
24
25 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
26
27 node = p;
28
29 //如果没有有那么幸运,对下挂的点检测。
30
31 else if ((e = p.next) != null) {
32
33 //如果是树,调用树的搜索方式,找到这个KV对。
34
35 if (p instanceof TreeNode)
36
37 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
38
39 //如果是一个链表,那么使用链表的查找方式,找到这个节点KV对,然后赋给node。
40
41 else {
42
43 do {
44
45 if (e.hash == hash &&
46
47 ((k = e.key) == key ||
48
49 (key != null && key.equals(k)))) {
50
51 node = e;
52
53 break;
54
55 }
56
57 p = e;
58
59 } while ((e = e.next) != null);
60
61 }
62
63
64
65 }
66
67
68
69 //如果在上面的步骤里面找到了node,并在逻辑上使用上 V 和 matchValue 判断找到的V是否有效。
70
71 if (node != null && (!matchValue || (v = node.value) == value ||
72
73 (value != null && value.equals(v)))) {
74
75 if (node instanceof TreeNode)
76
77 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
78
79 else if (node == p)
80
81 tab[index] = node.next;
82
83 else
84
85 p.next = node.next;
86
87 ++modCount;
88
89 --size;
90
91 afterNodeRemoval(node);
92
93 return node;
94
95 }
96
97 }
98
99 //没有找到node,下挂点为空或者是table为空,都会返回 null
100
101 return null;
102
103 }