1. 注释翻译
/**
* Associates the specified value with the specified key in this map.
* 将指定的值与此映射(地图)中的指定键相关联
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 如果地图先前包含了该键的映射,则替换旧值
* @param key key with which the specified value is to be associated
* 与指定值关联的键
* @param value value to be associated with the specified key
* 与指定键相关联的值
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
* 前一个值与key相关联 ,或null如果没有key的映射。(A null返回也可以指示以前关联的地图null与key。)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
// hash(key):根据key计算了当前Node的hash值,用于定位对象在那个结点上
}
通过注释我们可以了解到该方法的作用是:将指定的值与此映射中的指定键相关联。 如果地图先前包含了该键的映射,则替换旧值。深入put方法中发现只调用了putVal方法,我们进入到putVal方法中再去观察一下。
2. 源码剖析
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* 如果该值为true,则不需要改变现有的value值
* @param evict if false, the table is in creation mode.
* 如果该值为false,则表处于创建模式
* @return previous value, or null if none
* 如果key键有映射value,则替换就value,并返回旧value;如果没有则返回null
*/
// 此处的hash值是通过key值计算出的位置
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 这里的tab为null时,证明哈希表还未初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize方法进行哈希表的初始化操作
n = (tab = resize()).length; // 将初始化的哈希表容量赋给n
// 当前需存放KV值的桶为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建Node结点(KV值已存放),作为桶的头节点
tab[i] = newNode(hash, key, value, null);
else {
// 哈希表已经初始化,且当前需存放KV值的桶不为空
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) {
// 找到最后一个结点,将当前Node结点挂在链表后
p.next = newNode(hash, key, value, null);
// 如果当前链表个数到达树化阈值,调用树化方法treeifyBin将链表树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 结点已经挂载且链表已树化(达到树化阈值),跳出循环
break;
}
// 当前链表中存在相同的key,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 实现往后指向
p = e;
}
}
// 链表存在相同key,直接覆盖value,返回旧value
if (e != null) { // existing mapping for key
// 保留旧value值
V oldValue = e.value;
// 如果onlyIfAbsent值为true或oldValue值为null,则不需要替换旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// HashMap被结构性修改的次数加1(结构性修改是指改变了KV映射数量的操作或修改了HashMap的内部结构)
++modCount;
// 当添加结点后整个HashMap的元素若超过容量时,进行扩容
if (++size > threshold)
resize(); // 扩容
afterNodeInsertion(evict);
return null;
}
注意:
-
我们深入到putVal中会发现resize()方法有两个任务:创建初始存储数组(数组为null时)、扩容(容量不满足需求时)。这个方法我在其他博文有总结,如果有兴趣的,可以单击查看。
-
具体键值对在哈希表中的位置并不是由key值通过hash()方法计算的数组,而是通过
(n - 1) & hash
,这样我们会发现会将高位的数据移到低位,这是因为有些数据计算出的哈希值差异主要在高位,而HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。
3. 方法描述
put方法将KV放在map中。如果,该key已经存放在map中,则用新的value值直接替换旧value值。
如果该key已经存放在map中,则返回其映射的旧值;如果不存在,则返回null,表示没有该key的映射,也有一种特殊的情况,原来的映射是key-null,也会返回null。
put方法存储元素有两种情况:① 键值对在当前桶中是以链表形式存放的。② 键值对在当前桶中是以红黑树形式存放的。所以在插入时就需要判断当前桶的存储形式,需要特别关注链表存储时,超过TREEIFY_THRESHOLD,要进行树化操作。
4. put执行流程
- 若HashMap还未初始化,调用resize进行初始化
- 通过hash值得到所在桶的下标
① 若桶为空,将节点直接作为桶的头节点保存
② 若桶不为空
a. 如果桶中链表已经树化,使用树的方式添加结点
b. 未树化,将新节点以链表的形式尾插到最后
i. 添加元素后,链表个数binCount >= 树化阈值-1,尝试进行树化操作
③ 添加元素后计算整个哈希表的大小,若超过threshold(容量*负载因子),进行resize扩容操作。