HashMap 的 put 原理及 JDK7 与 JDK8 对比
HashMap 的 put 原理及 JDK7 与 JDK8 对比文档
一、HashMap 的 put 方法核心原理
HashMap 是 Java 中常用的键值对存储结构,基于哈希表实现,其put方法的核心作用是将键值对 (key-value) 存储到集合中。主要流程包括:
- 计算哈希值:根据 key 的 hashCode () 计算哈希值,用于确定元素在数组中的存储位置
- 确定桶位置:通过哈希值计算数组索引(桶位置)
- 处理哈希冲突:当多个元素映射到同一桶位置时,使用链表或红黑树存储
- 扩容机制:当元素数量达到阈值时,对哈希表进行扩容以保证性能
二、JDK7 与 JDK8 中 HashMap 的 put 方法对比
|
特性 |
JDK7 |
JDK8 |
|
数据结构 |
数组 + 单向链表 |
数组 + 单向链表 + 红黑树 |
|
哈希计算 |
多次扰动处理 |
简化扰动处理,保留高位信息 |
|
链表插入方式 |
头插法 |
尾插法 |
|
扩容时链表迁移 |
原顺序倒置 |
保持原顺序 |
|
阈值判断 |
先判断是否需要扩容,再插入 |
先插入,再判断是否需要扩容 |
|
冲突解决优化 |
仅链表 |
链表长度 > 8 时转为红黑树 |
|
并发问题 |
可能产生循环链表 |
避免了循环链表,但仍非线程安全 |
三、JDK8 中 HashMap 的 put 方法源码注释
|
/** * 在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系, * 则旧值被替换。 * * @param key 与指定值相关联的键 * @param value 与指定键相关联的值 * @return 以前与 key 相关联的值,如果没有 key 的映射关系,则返回 null。 * (返回 null 还可能表示该映射以前将 null 与 key 相关联。) */ public V put(K key, V value) { // 调用hash(key)计算键的哈希值,然后调用putVal方法 return putVal(hash(key), key, value, false, true); } /** * 计算键的哈希值,用于分布到数组中 * 对key的hashCode进行扰动处理,减少哈希冲突 */ static final int hash(Object key) { int h; // 如果key为null,哈希值为0,否则将key的hashCode与自身右移16位进行异或运算 // 目的是混合哈希码的高位和低位,增加低位的随机性,减少冲突 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * put方法的实际实现 * * @param hash key的哈希值 * @param key 键 * @param value 要放入的值 * @param onlyIfAbsent 如果为true,则不改变已存在的值 * @param evict 如果为false,表示处于创建模式 * @return 以前的值,如果没有则返回null */ 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. 初始化哈希表(table为null或长度为0时进行初始化) if ((tab = table) == null || (n = tab.length) == 0) // 调用resize()方法进行初始化,返回初始化后的数组长度n n = (tab = resize()).length;
// 2. 计算桶位置并检查是否为空 // (n - 1) & hash 等价于 hash % n,但位运算效率更高 if ((p = tab[i = (n - 1) & hash]) == null) // 如果桶为空,直接在该位置创建新节点 tab[i] = newNode(hash, key, value, null); else { // 3. 桶不为空,处理哈希冲突 Node<K,V> e; K k;
// 3.1 检查桶中第一个节点的key是否与当前key相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果key相同,记录下该节点,后续用于替换值 e = p; // 3.2 检查当前节点是否是红黑树节点 else if (p instanceof TreeNode) // 如果是红黑树,则调用红黑树的putTreeVal方法插入节点 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 3.3 不是红黑树,遍历链表 for (int binCount = 0; ; ++binCount) { // 到达链表尾部 if ((e = p.next) == null) { // 在链表尾部插入新节点(尾插法,与JDK7的头插法不同) p.next = newNode(hash, key, value, null); // 检查链表长度是否达到阈值(8),如果达到则将链表转为红黑树 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; } }
// 4. 如果找到了相同的key(e != null) if (e != null) { // existing mapping for key V oldValue = e.value; // 根据onlyIfAbsent参数决定是否替换旧值 if (!onlyIfAbsent || oldValue == null) e.value = value; // 访问后回调,用于LinkedHashMap实现 afterNodeAccess(e); // 返回旧值 return oldValue; } }
// 5. 没有找到相同的key,增加修改次数 ++modCount;
// 6. 检查是否需要扩容 if (++size > threshold) resize();
// 插入后回调,用于LinkedHashMap实现 afterNodeInsertion(evict); // 没有旧值,返回null return null; } |
四、JDK8 中 HashMap 的 put 方法核心改进点解析
- 引入红黑树:当链表长度超过 8 时,自动转换为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n),极大提升了大量哈希冲突时的性能
- 尾插法插入:相比 JDK7 的头插法,避免了多线程环境下可能出现的链表循环问题,同时保持了链表的插入顺序
- 哈希计算优化:简化了哈希计算的扰动次数,通过将高 16 位与低 16 位异或,在保证哈希分布均匀性的同时提高了计算效率
- 扩容机制优化:扩容时通过高低位拆分的方式迁移节点,避免了 JDK7 中重新计算哈希值的操作,同时保持了链表的原有顺序
- 流程调整:先插入元素再判断是否需要扩容,与 JDK7 的先判断后插入不同,逻辑更清晰
这些改进使得 JDK8 的 HashMap 在性能和稳定性上都有了显著提升,尤其是在处理大量数据和哈希冲突较多的场景下表现更为优异。
流程步骤详解
计算键的哈希值
本文来自博客园,作者:诸葛匹夫,转载请注明原文链接:https://www.cnblogs.com/shenxingzhuge/p/19069667
卡里离冰冷的40个亿还差39多个亿
浙公网安备 33010602011771号