HashMap 添加元素put()的源码和扩容方法resize()的源码解析
HashMap.java
中 put
的源码
public V put(K key, V value) {// 调用核心方法 putVal,传入参数:hash(key)、key、value// 第四个参数 onlyIfAbsent = false(即允许覆盖旧值)// 第五个参数 evict = true(用于 LinkedHashMap 的扩展,这里恒为 true)return putVal(hash(key), key, value, false, true);
}
核心方法 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;// ① 如果哈希表数组 table 还没初始化,就调用 resize() 初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// ② 根据 hash 计算下标 (n - 1) & hash,找到存储位置// 如果该位置为空,直接新建一个节点放进去if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;// ③ 桶位不为空,分 3 种情况:// 情况一:当前节点和新 key 相同(hash 相等 && key 相等),则 e 指向该节点if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 情况二:该节点是红黑树节点(TreeNode),说明当前桶是红黑树结构// 调用树的 putTreeVal 方法进行插入else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 情况三:该桶是链表结构,遍历链表else {for (int binCount = 0; ; ++binCount) {// 遍历到链表尾部还没找到 key,则插入新节点if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 如果链表长度达到阈值 (>= 8),则转换成红黑树结构if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}// 遍历过程中如果找到相同 key,e 指向该节点if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;// 指针后移,继续遍历p = e;}}// ④ 如果 e 不为空,说明找到了相同 key 的节点if (e != null) {V oldValue = e.value;// 如果 onlyIfAbsent = false(允许覆盖)或者旧值为 null,则替换成新值if (!onlyIfAbsent || oldValue == null)e.value = value;// LinkedHashMap 的回调,HashMap 默认空实现afterNodeAccess(e);return oldValue; // 返回旧值}}// ⑤ 修改次数加 1,用于 fail-fast 机制++modCount;// ⑥ 如果元素个数超过阈值(capacity * loadFactor),触发扩容if (++size > threshold)resize();// ⑦ LinkedHashMap 的回调,HashMap 默认空实现afterNodeInsertion(evict);return null; // 插入新 key 返回 null
}
方法签名和参数解释
/*** @param hash key 的哈希值,已经过 HashMap 的扰动函数处理* @param key 要插入的键* @param value 要插入的值* @param onlyIfAbsent 如果为 true,则只有在 key 不存在时才插入;如果 key 已存在则不覆盖旧值* @param evict 用于 LinkedHashMap 的回调,HashMap 中无实际作用,一般传 true* @return 如果 key 已存在,返回旧值;如果是新插入的 key,返回 null*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
方法内部变量逐个解释
// 哈希表的数组引用,类型是 Node<K,V>[],即存放哈希桶的数组
Node<K,V>[] tab;// 当前桶位上的第一个节点,可能是 null、链表头、或红黑树根节点
Node<K,V> p;// 当前哈希表的容量(数组长度)
int n;// 计算出的桶下标,等于 (n - 1) & hash
int i;
关键逻辑
// 如果 table 还没有初始化(即第一次 put 时),需要进行初始化或扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 根据 hash 值和数组长度计算桶下标i = (n - 1) & hash;// 找到桶位上的第一个节点p = tab[i];// ① 如果该桶是空的,直接新建节点并放入数组if (p == null) {tab[i] = newNode(hash, key, value, null);} else {// e 用来指向可能存在的相同 key 的节点(即旧节点)Node<K,V> e;// k 临时变量,用于存储 p 的 keyK k;// ② 如果桶位上的第一个节点与待插入的 key 相同(hash 相等且 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 {e = null;for (int binCount = 0; ; ++binCount) {// 如果到达链表末尾还没找到相同 key,则在尾部新建节点if (p.next == null) {p.next = newNode(hash, key, value, null);// 如果链表长度超过阈值(>=8),则转为红黑树if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}// 如果找到相同 key 的节点,则保存到 e 并退出循环if (p.next.hash == hash &&((k = p.next.key) == key || (key != null && key.equals(k)))) {e = p.next;break;}// 向后移动指针p = p.next;}}// ⑤ 如果找到了相同 key 的节点(e != null)if (e != null) {V oldValue = e.value;// 如果允许覆盖(onlyIfAbsent = false)或者旧值为 null,则更新 valueif (!onlyIfAbsent || oldValue == null)e.value = value;// LinkedHashMap 的回调(HashMap 默认空实现)afterNodeAccess(e);return oldValue; // 返回旧值}}// 修改次数 +1,用于 fail-fast 机制++modCount;// 元素数量 +1,如果超过阈值则扩容if (++size > threshold)resize();// LinkedHashMap 的回调(HashMap 默认空实现)afterNodeInsertion(evict);// 新插入的 key 返回 nullreturn null;
}
总结 put
方法主要逻辑
-
懒加载:第一次使用时初始化哈希表(
resize()
)。 -
计算桶下标:通过
(n - 1) & hash
定位。 -
三种情况处理:
- 桶为空:直接插入新节点。
- 桶非空:判断是否相同 key;如果是红黑树则插入树;否则遍历链表插入或更新。
- 链表长度过长则树化(转为红黑树)。
-
覆盖逻辑:如果 key 已存在,是否覆盖取决于
onlyIfAbsent
参数。 -
扩容:超过阈值(
threshold
=capacity * loadFactor
)则触发resize()
。 -
返回值:如果是新 key,返回
null
;如果是旧 key,返回旧值。
resize()
方法源码
/*** 扩容方法:当哈希表第一次初始化 或 元素个数超过阈值时调用。* 核心逻辑:* 1. 如果是第一次使用,初始化数组;* 2. 如果元素个数超过阈值,则容量扩容为原来的两倍;* 3. 重新计算阈值(threshold = capacity * loadFactor);* 4. 把旧数组里的节点迁移到新数组中(rehash)。*/
final Node<K,V>[] resize() {// 保存旧表引用Node<K,V>[] oldTab = table;// 保存旧容量(数组长度)int oldCap = (oldTab == null) ? 0 : oldTab.length;// 保存旧阈值int oldThr = threshold;// 新容量(newCap)和新阈值(newThr),待计算int newCap, newThr = 0;// ① 如果旧数组已经有容量if (oldCap > 0) {// 如果旧容量已经达到最大值 (2^30),则不再扩容,只能设为最大值if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE; // 不再扩容return oldTab; // 直接返回旧表}// 否则新容量扩容为原来的 2 倍,同时阈值也变为原来的 2 倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY) {newThr = oldThr << 1; // threshold 也翻倍}}// ② 如果旧容量为 0,但是旧阈值大于 0// 说明之前通过构造函数指定过容量,但 table 还没初始化else if (oldThr > 0) {newCap = oldThr; // 直接用旧阈值作为新容量}// ③ 如果旧容量为 0,旧阈值也为 0// 说明是第一次调用 putelse {newCap = DEFAULT_INITIAL_CAPACITY; // 默认容量 16newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值 = 16 * 0.75 = 12}// ④ 如果新阈值还没确定,就用负载因子计算if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY? (int)ft : Integer.MAX_VALUE);}// 更新全局 thresholdthreshold = newThr;// 创建新表@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// ⑤ 如果旧表不为空,需要把旧节点迁移过来if (oldTab != null) {// 遍历旧表for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null; // 释放旧桶,帮助 GC// 如果桶中只有一个节点,直接搬到新表if (e.next == null) {newTab[e.hash & (newCap - 1)] = e;}// 如果是红黑树,拆分到新表else if (e instanceof TreeNode) {((TreeNode<K,V>)e).split(this, newTab, j, oldCap);}// 如果是链表,则需要拆分成两个链表else {// 低位链表(扩容后仍然在原索引位置 j)Node<K,V> loHead = null, loTail = null;// 高位链表(扩容后在 j + oldCap 位置)Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 根据 (e.hash & oldCap) 判断是放低位还是高位if ((e.hash & oldCap) == 0) {if (loTail == null) loHead = e;else loTail.next = e;loTail = e;} else {if (hiTail == null) hiHead = e;else hiTail.next = e;hiTail = e;}} while ((e = next) != null);// 把低位链表放在原位置if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 把高位链表放在 j + oldCap 位置if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}
扩容的关键点总结
-
触发时机
- 第一次调用
put
时(table 还没初始化)。 - 元素个数超过阈值时(
size > threshold
)。
- 第一次调用
-
容量变化规则
- 默认初始容量是 16,负载因子是 0.75,阈值 = 12。
- 每次扩容为原来的 2 倍。
-
rehash 过程优化
-
在 JDK 1.7 之前,需要重新计算每个元素的 hash,比较耗时。
-
JDK 1.8 引入了 高低位链表拆分优化:
- 根据
(e.hash & oldCap)
判断元素是留在原索引,还是移动到原索引 + oldCap
。 - 不需要重新计算 hash,提高了效率。
- 根据
-
-
树节点处理
- 如果桶里是红黑树,调用
TreeNode.split()
把树拆分到新桶中。
- 如果桶里是红黑树,调用