final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node 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)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;
}
下面我们开始分析putVal这个方法.这里是重点 这里是重点 这里是重点
第一个if实际上就是执行了一个赋值的过程,给tab和n赋值.先将HashMap中的table赋值给tab,将table.lenght赋值给你,如果tab是null,或者n是0,resize()方法重新赋值.resize()方法我们一会在说,我们接着往下看第二个if.
这个if又干了什么呢? 这里我们可以先看如果这个if条件如果为true会执行什么,然后在看条件判断这部分.
先看tab[i] = newNode(hash, key, value, null); 这行代码我感觉挺简单的,之前将HashMap中的table赋值给了tab,并且做了一些处理之后才赋值的,确保table不为null,长度大于0.那么tab就是一个Node类型的数组,这里就是根据你传给putVal方法的值创建了一个Node对象,然后赋值给tab[i]这个位置.
ok,这个时候我们可以继续看上面的条件什么时候为true了,(i的这个值,也是在条件判断的时候赋值的)
if ((p = tab[i = (n - 1) & hash]) == null);
先看这行代码中的几个属性:
p 是Node类型的对象, 但是没有初始化
tab 就是table赋值过来的Node类型的数组
n 就是table的length
在看这行代码的执行顺序.我们把这行代码拆分成几个部分
第一个 执行的是 (n-1) 得到的结果是15
table的初始长度是16,此时我们先把n当做16,为了保证我们的思路能够清晰,这里先不看别的方法.不信你可以自己去验证,或者给我留言
第二个 执行的是 i = (n - 1) & hash 也就是15 与一个 int 类型的整数进行 按位与 运算的结果赋值给 变量 i
到这里就比较有意思了,我们知道 i 代表的是tab这个数组也可以说是table这个数组中的下标,那么这个下标是怎么来的呢?
首先说 n 代表的是数组的长度,那么当n等于16的时候,最大最大下标的位置就是 15 ,也就是说只有 0-15 之间的数可以做这个数组的下标
到这里,也就是说我想给 i 这个变量赋值一个0-15之间的下标, 然后通过这个下标去数组中取值 给 变量 p 赋值. 我们考虑一下这个 i 下标需要满足什么特性?
a. i >= 0 && i <= 15
b. 我下次再put值的时候, 这个i的值应该是相同的.
我们看下HashMap是怎么实现的. (n - 1) & hash
a. 现在已知 n-1 = 15 , hash是一个整数 ,我们来进行按位与运算
b.怎么保证 i 下次再计算的时候 i 的值是不变的 , 这个时候我们就需要看这个这两个运算数是怎么来的, 15 根据数组长度来的,只要数组长度不变,这个就绝对不会变(如果数组长度改变的话,涉及到扩容,存储的位置也可能发生变化),另外一个hash变量是跟你你传入的key的hashCode()方法返回的hashCode码得来的,java中同一个对象多次调用hashCode()方法返回的值是相同的,除非你重新了hashCode()方法.
c.这种算法的好处,首先能够保证其运算性能,第二还在HashMap进行扩容的时候提供了一定的方便,这个后面我们在说.
**第三个** 执行的就是(p = tab[i = (n - 1) & hash]) 相当于p = tab[i] ,在数组中取值,然后赋值给 p .
第四个 执行的就是 if ((p = tab[i = (n - 1) & hash]) == null); 也就是最终判断 p == null
到这里第二个if就要分析完了, 第二个if干了什么,实际上就是通过一系列算法根据put的key值,取得数组中对应位置的Node对象,如果这个对象为null,也就是数组中这个位置没有值,那么就根据传入的参数创建一个Node对象,放在这个位置.有值就进行else操作.大概的流程图
首先跟大家说声抱歉,这篇文章写的有点虎头蛇尾了,HashMap中的知识点实际上挺多的,我本意是打算写的详细一点,那种小白都可以看的懂的,但是写的太详细了,如果都在一篇文章中可能篇幅会有点大.如果你看到这里,感觉前面写的还算清楚的话,想继续了解关于HashMap的一些细节,欢迎留言.我会抽出时间接着写下去关于你提出的问题.
还是说啊,源码是最好的文档,多看看优秀的源码对你的帮助一定是很大的.接下来简单总结下HashMap可能问道的一些面试题.还是说,怎么实现的你都清楚了,面试题应该就不存在问题了.
HashMap中的table的默认大小是16
扩容是扩容2倍,(一定是2n)
resize()功能是初始化和扩容.
扩容后的数据转存三种情况
1.table[]数组中的这个位置不为null,且next下为null,直接重新添加
2.table[]数组中的这个位置不为null,且next下不为null,类型是红黑树,打散重新添加
3.table[]数组中的这个位置不为null,且next下不为null,类型是链表,重新计算位置.
计算位置方式,只有两种可能,一种是原来的位置,另一种是原来的位置+原来数组的长度.
用hash码与原来数组长度进行&运算结果为0就是原来的位置.不为0就是原来的位置加上原来数组长度的位置.