HashMap1.8hash碰撞 和 扩容方法

为何要学习hashMap的源码
因为集合在我们工作和学习过程中都非常常见,且代码写的非常优雅,如果想要得到一份高工资的工作,且现在市
面上jdk1.8已经流行起来了,相信面试过程中越来越多的面试官会询问源码的知识,所有源码是我们必须去弄懂
的,接下来我们就来一起学习hashMap1.8的源源码前的设想问题
思考的问题
1. hashMap初始化大小是多少
2. hashMap结构是什么样子
3. 如果已经得知hashMap的数据结构是链表加数组,那我们如何去避免hash碰撞
4. hashMap在什么时候扩容
5. 扩容的的方式是什么
正式篇
hashMap的结构
1.在1.8中,hashMap的结构分成链表加数组和数组+红黑树,因为1.8的作者考虑到链表没有索引,遍历效率低
下,所以当链表长度大于 8 - 1 也就是 7 时,会转化成红黑树,那么当长度不足6时,又会将红黑树转化成链表 在
源码中 描述数据结构的样子是transient Node<K,V>[] table; 而node是一个单链表对象,所以结构是数组+链表  
 

[Java] 纯文本查看 复制代码
1
2
static final int TREEIFY_THRESHOLD = 8; 树的临界点
static final int UNTREEIFY_THRESHOLD = 6;非树的临界点


hash碰撞问题
1.既然已经知道了hashMap的结构,那么接下来,我们就来看看他如何放置hash碰撞问题,按照我们的设想,我
们可以让其对数组的长度取模,比如,数组的长度是16,我们可以用key的hash值对数组取模,取到的值是0-15正
好能落在数组的位置上,但这种方式并不能保证数字能够尽量分散的落在数组上,而过多的元素落在同一个节点上
就会导致形成的链表长度过长,而影响hashmap的取值速度,所以我们现在就来看看源码中是如何实现的

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} i
f ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


综合来看,此方法两个参数参与了运算
option1:hash值
option2:数组的长度-1 那么数组的长度是 ,且在源码注释处已经指出,数组的长度需要是2^n次幂,即偶数
一个int型的hash值 ,我们可以随机假设一个:
010101010101001010101010101010
那么它与一个偶数或者一个非偶数取&操作会是什么结果呢?
如果是和奇数取&那么得到的结果即可能是奇数,也可能是偶数,而如果和一个偶数&那么答案只能是一个偶数,
所以答案很明显,只有当数组的长度是偶数即2的n次幂时,此时才能保证得到的数字即可能是偶数也可能是奇数
那我们再来看第一个值,第一个值是hash值,hash值只能和数组的长度-1 运算,那么如果是假设直接拿hash值和
数组计算,相当于这个hash值只有几位参与了运算,其他位并没有参与运算,这样做可能也会使得不同的元素,得
到相同的结果(即最后几位相等,但前几位不同),在源码中采用的办法是将hash值向右移动16位,得到他的高16
位,同时和低16 位取异或,这样就能保证整个hash值都参与了运算,那为啥取异或呢?因为异或可以使得得到的
0,1 二进制尽量的平均
举例
0 0 1 1
0 1 0 1 取&
0 0 0 1 0 的概率0.75 1 的概率0.25
0 0 11
0 1 0 1 取|
0 1 1 1 0 的概率0.25 1 的概率0.75
0 0 1 1
0 1 0 1 取^
0 1 1 0 的概率 0.5 1的概率0.5
hash的扩容方法
在 1.8中hashMap的扩容方法是由 resize方法决定的
此方法两个作用
1.初始化
2.扩容

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
{
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;
} e
lse {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
} i
f (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}


此方法就是决定扩容核心代码
首先遍历这个数组,遍历过程中有三段逻辑
1.看这个数组上的元素是否为null,如果是null,那么采用尾插法(1.7采用的是头插法,但头插在多线程情况下可能
出现链表的死循环,这里不作多解释)
2.如果是红黑树,那么按照红黑树的处理方式处理
3.如果数组上有元素,按照我们的想法,我们应当是重新计算这个key在数组中的位置
最核心的代码: 是 e.hash & oldCap
我们发现当它在计算时,它并没有直接和oldCap -1 计算& 而是直接和数组的长度计算
那么我们再次带入这个计算的方式看看这个精妙的算法是怎么回事:
当第一次扩容时,执行的resize方法

[Java] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
9
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} e
lse if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}


此时数组的长度已经扩大了两倍
新长度是32 ,老长度是16
0101010101010010101010101 0 1 0 1 0
0 1 1 1 1 老长度-1 //1
1 0 0 0 0 老长度 //2
0 1 1 1 1 1 新长度-1 //3
用 1,3对比,我们可以发现,数组移动不移动是看第五位是否是0 ,如果是0 ,不移动,如果不是0 ,新长度的值要
比原来的值 大16
用2,3 对比,如果第五5位是0,那么得到的结果就是0 ,如果第五位非0 ,那么得到的结果就不是0
1,3移动---> 第五位非0 ,且移动16 位 ---> 第五位 0 , 不移动
2,3 ---> 第五位非0 ,知道要移动了,且在源码中可以发现,移动了16
第五位 0 ,不移动

[Java] 纯文本查看 复制代码
1
2
3
4
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}


所以移动不移动,只需要看倒数第五位即可,不得不说,hashMap这个设计很精妙
结束语
相信通过刚才的学习,同学们已经对hash的碰撞问题和hash的扩容方法有了一个具体的认识,希望大家继续认真
学习。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章