put( ) 方法用于向 HashMap 中插入一个键值对,如果键已存在,那么就替换原来的值,如果键不存在,那么就创建一个新的节点并插入到 HashMap 中。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 数组该位置有数据
Node<K,V> e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
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) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
代码中的变量和方法有以下含义:
- hash: 用于计算键的哈希值,用于确定键在数组中的位置
- key: 要插入或查找的键
- value: 要插入或返回的值
- onlyIfAbsent: 一个布尔值,表示是否只在键不存在时才插入值
- evict: 一个布尔值,表示是否在插入后调用 afterNodeInsertion 方法
- tab: HashMap 的内部数组,存储 Node 节点
- p: 一个 Node 变量,用于遍历链表或树
- n: 数组的长度
- i: 数组的索引
- e: 一个 Node 变量,用于存储找到或创建的节点
- k: 一个临时变量,用于存储节点的键
代码中的逻辑步骤如下:
- 检查数组是否为空或长度为零,如果是,则调用 resize 方法初始化或扩容数组,并更新 n 的值。
- 根据 hash 和 n 计算 i 的值,并从数组中取出 i 位置上的节点 p。
- 如果 p 为空,说明该位置没有数据,直接创建一个新节点并赋给 p。
- 否则,说明该位置有数据,则进入 else 分支。
- 首先判断 p 的哈希值和 key 是否和传入的参数相等(使用 == 或 equals 方法),如果是,则说明找到了相同的键,将 p 赋给 e。
- 否则判断 p 是否是 TreeNode 类型(一种特殊的 Node 类型,用于实现红黑树),如果是,则调用 putTreeVal 方法在树中查找或插入节点,并将返回值赋给 e。
- 否则进入 for 循环,在链表中遍历查找或插入节点。每次循环都会增加 binCount 的值,并检查 e 是否为空。如果为空,则说明没有找到相同的键,在链表尾部创建一个新节点并赋给 e,并且判断 binCount 是否超过了 TREEIFY_THRESHOLD(默认为8),如果是,则调用 treeifyBin 方法将链表转换为树结构。然后跳出循环。如果不为空,则继续判断 e 的哈希值和 key 是否和传入参数相等(使用 == 或 equals 方法),如果是,则说明找到了相同的键,并跳出循环。否则将 e 赋给 p 并继续循环。
- 如果 e 不为空,则说明找到了相同的键或者创建了新节点。取出 e 的旧值 oldValue,并根据 onlyIfAbsent 参数决定是否更新 e 的 value 值。然后调用 afterNodeAccess 方法进行一些后续操作(比如移动节点到链表头部)。最后返回 oldValue。
- 增加 modCount 的值(表示 HashMap 结构被修改了多少次)。
- 如果 size 超过了 threshold(阈值,默认为数组长度乘以负载因子),则调用 resize 方法进行扩容操作。
- 调用 afterNodeInsertion 方法进行一些后续操作(比如删除最老的元素)。
- 返回 null。