简书 加薪猫
转载请注明原创出处,谢谢!
这一系列主要介绍HashMap(1.8),记录也是分享,欢迎讨论
上回书说到get方法的爱恨情仇,其实主要还是简单介绍了HashMap的一些基本结构,还有HashMap的哈希值是怎么算出来的,这些可以为我们的put方法打下一定的基础,今天就来分享put方法
2.put(K,V)
老样子,先怼源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法完结撒花,下面我们来讲putVal.(本猫一系列都将以JDK1.8为主讲述,如果有心人看源码时发现自己的源码可能是个假源码的请更换JDK版本,hash()方法可看上节)
putVal(int hash,K key,V value, boolean onlyIfAbsent,boolean evict)
先介绍这个方法的几个参数:
hash就是上节我们说过的,经过HashMap处理过的哈希值
key和value不谈,onlyIfAbsent这个参数的作用在于,如果我们传入的key已存在我们是否去替换,true:不替换,false:替换。
参数evict是用在LinkedHashMap的,这里暂且不谈,等之后分享LinkedHashMap的时候我们再来细细分析
我们先看putVal的代码
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)//③
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;
}
以上代码很长,加上手机上显示好像没有高亮,看着更是难受。所以读者可以以上面为机构,我会在下面拆分进项讲解。下面的拆分和上面的注解一起看,效果更加哦。
①
Node<K,V>[] tab; Node<K,V> p; int n, i;
第一次看源码的时候,很容易搞混变量,尤其是看在中间的时候会突然走个神就想着,哎?我是谁,我在哪,我在干什么。。所以最好的方式,还是在看代码时,当你看懂这个变量的意义的时候就加上自己的注释。
tab:HashMap里面存储Node节点的数组
p:当前要put进HashMap的key,经过计算得出的tab下标所对应第一个Node。(啥?我在说啥? 第一个Node的意思是,我们的Node本来就是链表中的一个节点,我们根据Key只能找到这个链表的头) 这个更精确地说是当前处理的节点
n:tab当前的大小
i:key算出来的下标
②
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
table是全局变量,先设置tab= table, n = tab.length,判断如果tab为null,或者tab的大小为0的话,我们就要调用resize()方法进行第一次扩容(resize()方法我们在下面详细说,这里你可以暂时理解为初始化HashMap),从这里我们可以知道的一点是HashMap不是在实例化话的时候就生成指定大小的Node<K,V>[],而是在第一次put时才会进行初始化。
③
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这里是我们根据计算的哈希值(具体计算方式可以看上一节)算出下标的方式,(n-1)& hash
,(15&3=3,15&18=2,与运算这里就不多说了哈~),如果这算出来的下标i,tab[i]为空的话,我们可以直接tab[i]下塞一个新的Node就好了。
④ 仿佛又是一大块,这也是这个函数重点理解的地方,⑤~⑪可以就看下面的代码,当然我上面也会标注的
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;
}
}
这一段主要讲的当算出的索引下标已经有值时HashMap的处理方式。
⑤
Node<K,V> e; K k;
e:如果要put进HashMap的key已存在,则e为这个key的节点,如果不存在则为null。
k:遍历链表时,当前节点的key
⑥
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
总是判断第一个,如果算出的hash值相同并且key相同(这个key相同是指要么是同一个对象,要么是equals返回true),那么e=p
然后直接进入⑪的判断
⑦
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果p已经是TreeNode类了(也就是已经转换成红黑树了)就直接调用红黑树的putTreeVal,这个方法之后我们再具体讲,这里只要知道如果是key不存在这个方法返回null,如果存在返回对应的节点进入⑪的判断
⑧
for (int binCount = 0; ; ++binCount) {
.....
}
这个循环主要是用来记录链表的数量,之后如果超过默认值(8),会将链表转换为红黑树
⑨
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
p.next==null的意思是指我们遍历链表已经找到最后一个节点了,那就是虽然算出下标下是有链表的,但是并没有相同key,所以直接创建新的节点放在链表的最后。同时,如果这个链表的长度已经大于默认值8就会将当前这个链表转换为红黑树,具体treeifyBin()方法我们之后分享红黑树的时候来分享。
⑩
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
如果要put的key已经被找到了,则直接进入⑪判断
⑪
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
一般我们的put的方法都是会直接覆盖旧值的,其实一开始就想说的就是put方法是有返回值的!如果有旧值返回旧值,没有旧值就返回null。afterNodeAccess这个方法是LinkedHashMap用的,这里先不涉及
⑫
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
首先代码能走到这里,是③为true或者是⑪为false,这两个是什么意思呢,意思是之前没有相同的key,也就是说HashMap的大小会+1,++modCount
暂且不提,我们先看后面的,如果此时size+1后比threshold大(这个变量的值我们在讲resize的时候会重新介绍这里你可以简单以为是 容量*0.75),则会扩容。
afterNodeInsertion()方法也是LinkedHashMap的方法,这里暂不做介绍,至此Put方法完成啦~~
本猫的期望就是能够让大家更加容易的学习源码理解源码,诸君如果有建议在下面留言哦~下期会讲resize()方法
我是加薪猫
技术是加薪的前提,生活是加薪的意义
技术等于生活