HashMap的get、put流程源码分析
get 方法源码如下
final Node<K, V> getNode(int hash, Object key) {Node<K, V>[] tab; // 哈希桶数组引用Node<K, V> first, e; // first: 桶内首节点; e: 遍历用临时节点int n; K k; // n: 数组长度; k: 临时存储节点的键// 1. 检查哈希表状态:数组非空且长度>0,且目标桶存在节点if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 2. 优先检查首节点:哈希值匹配 + 键匹配(引用或 equals)if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {return first;}// 3. 存在后续节点时,区分链表/红黑树处理if ((e = first.next) != null) {// 3.1 红黑树节点:调用树查找方法if (first instanceof TreeNode) {return ((TreeNode<K, V>) first).getTreeNode(hash, key);}// 3.2 链表节点:遍历查找do {if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {return e;}} while ((e = e.next) != null); // 遍历到链表末尾}}// 4. 未找到节点return null;}
get方法通过getNode实现键值对查找,核心是利用哈希定位与数据结构适配实现高效查询,步骤如下:
1.哈希值计算
调用hash(key)生成二次哈希值,确保与插入时的定位逻辑一致。
2.索引定位与节点查找
计算索引后,检查哈希表状态及目标位置:
哈希表为空、长度为 0 或索引位置无节点:返回null;
索引位置首节点键匹配:直接返回该节点;
首节点不匹配时,根据结构处理:
红黑树节点:调用getTreeNode从根节点开始按红黑树规则查找;
链表节点:遍历链表逐一比对,找到匹配节点则返回,否则返回null。
3.效率优化
优先判断首节点(最常访问),避免无效遍历;红黑树将查找复杂度从链表的 O (n) 降至 O (log n),平衡查询效率。
put 方法源码如下
// 核心插入方法final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K, V>[] tab; Node<K, V> p; int n, i;// 1. 哈希表未初始化或为空时,进行扩容(初始化)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 2. 计算索引,定位哈希桶,桶为空则直接插入新节点if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K, V> e; K k;// 3. 检查首节点是否匹配(哈希和键都匹配)if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 4. 首节点是红黑树节点,调用红黑树插入逻辑else if (p instanceof TreeNode)e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);// 5. 首节点是链表节点,遍历链表处理else {for (int binCount = 0; ; ++binCount) {// 遍历到链表尾部,插入新节点if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 链表长度达标,尝试转红黑树if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash);break;}// 找到已存在的节点,跳出遍历if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 6. 处理已存在的节点,更新值if (e != null) { V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e); return oldValue;}}// 7. 插入新节点后,检查是否需要扩容、更新计数等++modCount;if (++size > threshold)resize();afterNodeInsertion(evict); return null;}
put方法的核心是通过putVal实现键值对的插入或更新,整体流程围绕哈希计算、节点定位、结构适配及动态扩容展开,具体步骤如下:
1.哈希值计算
调用hash(key)对键进行二次哈希(高 16 位与低 16 位异或),减少哈希冲突,为后续定位存储位置提供依据。
2.哈希表初始化 / 扩容
若哈希表(table)未初始化(为空或长度为 0),通过resize()方法完成初始化(默认容量 16,负载因子 0.75,阈值 12);若后续元素数量超过阈值,resize()会将容量翻倍(确保为 2 的幂次方),维持高效存储。
3.索引定位与节点插入
利用(n-1) & hash计算存储索引(n为当前容量,位运算等价于取模且更高效),根据索引位置状态处理:
位置为空:直接创建新节点插入;
位置非空:
首节点键匹配(哈希值与键均相等):更新节点值;
首节点为红黑树节点:调用putTreeVal按红黑树规则插入;
首节点为链表节点:遍历链表,找到匹配节点则更新值,否则尾部插入新节点;当链表长度达到树化阈值(8)且容量≥64 时,调用treeifyBin转为红黑树(容量不足 64 时优先扩容)。
4.计数与扩容检查
插入新节点后,modCount(结构修改计数器)自增,size(元素总数)加 1;若size超过阈值,触发resize()扩容,保证哈希表性能。