/**
* Implements Map.put and related methods
*
* @param hash key的哈希值
* @param key the key
* @param value 待存入列表的值
* @param onlyIfAbsent 如果为真,不会改变值
* @param evict 如果为false,则表处于创建模式
* @return 之前的value, or null if none
*/
//注意:单独占用一行或者多行的注释,则是对它下方的代码进行解释。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //n用来存放数组的长度,i用来存放计算得的下标
if ((tab = table) == null || (n = tab.length) == 0) //判断数组是否为空
n = (tab = resize()).length; //初始化数组,长度默认时16
/*
n-1是为了获取的下标范围能够在数组长度之内。
因为数组的长度都是2的次方,当进行-1操作之后,低位都是1。在 &hash时得到的就是与都是1的低位 长度相同的原hash二进制的值
取个例子,如何数组长度为16时
十六的二进制:0001 0000
hash二进制:1011 0110
(16-1)&hash
0000 1111
1011 0110
根据&运算符的特性
结果就是 0110,0110的十进制就是数组下标。
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//直接存放到数组中。
else { //否则说明出现了hash冲突,也就是(n-1)&hash得到得下标对应得数组位置已经存放了结点。
Node<K,V> e; K k; //声明一个结点,以及与Key类型相同得变量,遍历结点时需要用到。
/*
在上一if中,p已经指向了tab[i = (n - 1) & hash]]的对象,也可以说是链表头。因为数组中
每一个元素都是链表结构。
这里的判断有三个
1、p.hash == hash: 链表头Node的hash是否与要插入的hash相同。
&
(
2、((k = p.key) == key:链表头Node的key是否与要插入的key相同
||
3、(key != null && key.equals(k)): key不为空。并将key的内容不与Node的相同。
)
2和3是一个组合也就说,两个判断中只要有一个满足条件就可以。
*/
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 { //当上面的条件都不满足时,也就是出现key相同或者链表已经转变红黑树的情况,就对 链表进行遍历
for (int binCount = 0; ; ++binCount) {
/*
如果当前结点为空,则将新的结点插入。
*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
/*
判断链表的长度是否达到数化的阈值。当链表的长度等于 >= 8时树化
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //执行树化的方法。
break;
}
/*
与检查链表头Node的判断相同,都是判断否存入的是同一个key。相同的话就直接退出
*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //获取下去结点的必备条件。 e = p.next; p = e;
}
}
/*
这里判断为真说明 新插入的结点的key与之前插入的结点重复
这里的操作就是,将该新的Value更换结点上的value,返回旧的value值
*/
if (e != null) { // existing mapping for key
V oldValue = e.value; //获取结点上的value
if (!onlyIfAbsent || oldValue == null) //判断是否可以更改值。
e.value = value;
afterNodeAccess(e); //回调。
return oldValue;//返回久的值
}
}
++modCount; //统计链表的修改次数
if (++size > threshold) //如果当前数组中的总结点数大于扩容阈值。对数组进行扩容
resize(); //扩容
afterNodeInsertion(evict);//转移
return null;
}
个人的理解:
上面的注释就是我对于JDK1.8的HashMap在进行Put的时候底层的实现原理,简单总结一下put的实现过程。
1、 调用put 的时候,传入两个变量 key 和 value,put会调用putVal方法 传入key计算得出的hash值、value、对结点的操作类型onlyIfAbsent(是否可以改变值)、数组目前的状态evict(该表是目前是否处于创建状态)。
2、 进入putVal之后,由于HashMap的数组是使用懒加载机制创建的,所以会先判断当前的数组是否已经初始化,如果未初始化则先进行数组的初始化,长度默认是16。
3、 数组初始化完成之后,使用传入的hash计算出该结点在数组中的下标。
4、 得出下标后,定位到数组中该下标的元素,判断是否已经存入值,也就是是否发生过hash冲突,如果没有就直接将值存储该下标的数组元素中,成为链表的头节点。
5、 如果定位到该下标的元素发现已经存有值,先对头结点进行判断,判断目前存入的key是否与其的key相等,如果相等则将新value更换原有的value。并返回原先的value。否则继续向下遍历链表,遍历过程中如果发现有相等的key则进行前面所说的更新value操作。如果遍历完整个链表发现都没有出现key冲突,则将创建新结点插入链表的尾部。
6、 插入新结点后判断链表的长度是否大于8,如果大于8则将链表进行红黑树化操作。
7、 判断当前数组中总结点的个数是否达到扩容的阈值,满足条件则对数组进行扩容,创建新的数组,将原数组中结点存入新的数组中。
这个是我个人在研究HashMap 进行put操作时底层实现原理的理解,如果大家在浏览过程中,发现有理解错误的地方可以在评论中留言,后面我再进行更改。
深入剖析JDK1.8中HashMap的put方法实现细节,包括冲突处理、链表转红黑树及扩容机制。
542

被折叠的 条评论
为什么被折叠?



