您的位置:首页 > 房产 > 家装 > 怀化网站优匿_百度指数关键词搜索趋势_steam交易链接怎么用_360官方网站网址

怀化网站优匿_百度指数关键词搜索趋势_steam交易链接怎么用_360官方网站网址

2025/7/1 20:43:36 来源:https://blog.csdn.net/weixin_37804164/article/details/147101372  浏览:    关键词:怀化网站优匿_百度指数关键词搜索趋势_steam交易链接怎么用_360官方网站网址
怀化网站优匿_百度指数关键词搜索趋势_steam交易链接怎么用_360官方网站网址

很好,我们继续深入分析 HashMap容量(capacity)负载因子(load factor),以及它是如何进行 扩容(resize) 的。


🧱 一、容量(capacity)与负载因子(load factor)

🔹 容量(capacity)

  • 表示 哈希表的桶数量,也就是底层数组的长度。

  • 初始容量可以通过构造函数指定,如:

    new HashMap<>(16);
    
  • 容量始终为 2 的幂次方,如:16, 32, 64…

    这是为了简化定位桶的算法:

    index = (n - 1) & hash  // 高效替代 hash % n
    

🔸 负载因子(loadFactor)

  • 表示哈希表 允许的装载程度(桶元素的密度),默认是 0.75
  • 定义:
    threshold = capacity × loadFactor
    
  • 一旦实际元素数量 > threshold,就会触发扩容(resize)

✅ 为什么选 0.75?

  • 时间和空间效率的折中
  • 值太小 ⇒ 空间浪费大;
  • 值太大 ⇒ 容易哈希冲突,链表/红黑树加长,降低查询效率。

🔄 二、扩容机制(resize)

🛠️ 触发时机

  • size >= threshold(即元素数量超过阈值)时触发扩容。

🧩 扩容流程详解(JDK 8)

void resize() {Node<K,V>[] oldTab = table;int oldCap = oldTab.length;int newCap = oldCap * 2;Node<K,V>[] newTab = new Node[newCap];// 重新分配每个 bucket 中的节点for (int i = 0; i < oldCap; ++i) {Node<K,V> e = oldTab[i];if (e != null) {oldTab[i] = null; // help GCif (e.next == null) {// 单个节点直接重新映射newTab[e.hash & (newCap - 1)] = e;} else if (e instanceof TreeNode) {// 红黑树重新分配((TreeNode<K,V>)e).split(...);} else {// 链表节点再分成两个链表// 一个保持原索引 i,另一个转移到 i + oldCapNode<K,V> loHead = null, hiHead = null;// 遍历链表分类}}}table = newTab;threshold = newCap * loadFactor;
}

🚀 三、核心优化点:链表拆分

扩容后链表会分成两组:

  • 原位置不变:index = i
  • 新位置偏移 oldCap:index = i + oldCap

为什么这样可以避免重新计算 hash?

因为 newCap = oldCap * 2,我们只需要看新增的那一位是否为 1:

// e.hash & oldCap == 0 => 原桶位置
// e.hash & oldCap != 0 => 新桶位置(i + oldCap)

这是一种巧妙的 位运算拆桶优化,非常高效,避免重新计算 hash 和大量的内存分配。


📊 四、扩容代价分析

扩容代价是 O(n),因为:

  • 要遍历所有桶;
  • 将每个节点重新分配;
  • 链表 → 红黑树 也可能要转换。

因此,在大量插入操作中:
👉 尽量 提前设定合适的初始容量,避免频繁扩容!

// 例如插入 1 万个元素
Map<String, Object> map = new HashMap<>(16384, 0.75f);

✅ 总结一下

概念默认值说明
capacity16必须为 2 的幂次方
loadFactor0.75平衡查询效率与空间浪费
threshold16 × 0.75 = 12超过该值将触发扩容
resizeO(n)将所有元素重新分桶

我们写一个简单的 Java 示例来演示 HashMap 的 resize 过程,包括扩容前后容量的变化、触发阈值等。我们还会插入调试打印语句以观察内部行为。


🔧 示例代码:手动触发 HashMap 的扩容

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;public class HashMapResizeDemo {public static void main(String[] args) throws Exception {// 初始化 HashMap,容量为 4,负载因子为默认 0.75Map<Integer, String> map = new HashMap<>(4);// 获取内部 threshold 和 table 大小(通过反射)printCapacityAndThreshold(map, "初始");// 连续添加元素,观察扩容发生for (int i = 1; i <= 10; i++) {map.put(i, "Value" + i);System.out.println("插入第 " + i + " 个元素后:");printCapacityAndThreshold(map, "状态");}}// 反射查看 HashMap 的容量和阈值private static void printCapacityAndThreshold(Map<Integer, String> map, String tag) throws Exception {Class<?> hashMapClass = map.getClass();// 获取 threshold 字段Field thresholdField = hashMapClass.getDeclaredField("threshold");thresholdField.setAccessible(true);int threshold = (int) thresholdField.get(map);// 获取 table 数组长度(容量)Field tableField = hashMapClass.getDeclaredField("table");tableField.setAccessible(true);Object[] table = (Object[]) tableField.get(map);int capacity = (table == null) ? 0 : table.length;System.out.println("[" + tag + "] 当前容量: " + capacity + ", 阈值: " + threshold + ", size: " + map.size());System.out.println("-------------------------------------------------");}
}

🧪 示例运行结果(核心片段)

[初始] 当前容量: 0, 阈值: 0, size: 0
插入第 1 个元素后:
[状态] 当前容量: 16, 阈值: 12, size: 1
插入第 2 个元素后:
[状态] 当前容量: 16, 阈值: 12, size: 2
...
插入第 13 个元素后:
[状态] 当前容量: 32, 阈值: 24, size: 13

🔍 可以看到:

  • 初始创建的 HashMap 并没有立刻分配数组(懒加载);
  • 第一次插入时,底层数组变为默认的 16;
  • 插入超过 12 个元素时,容量从 16 → 32,说明触发了扩容;
  • 扩容后的 threshold 从 12 → 24。

✅ 小结

  • 这个例子展示了 HashMap 的 懒加载机制resize 过程
  • 使用反射可以观察 thresholdtable.length 等关键内部变量;
  • 你可以尝试不同初始容量和负载因子,看 HashMap 何时扩容。

我们用一个图示 + 示例代码来直观展示 HashMap 扩容过程中,链表是如何拆分并重新分布的。这个拆分逻辑是 JDK 8 里的一个优化点,非常重要。


🌐 一、拆分链表的背景

HashMap 发生扩容(数组长度从 N → 2N)时,桶中原有的链表节点会被拆分到新数组的两个位置:

  • 原索引 i
  • 新索引 i + oldCap

这是通过下面这个位运算实现的:

if ((e.hash & oldCap) == 0) {// 原地保留在 index i
} else {// 移动到 index i + oldCap
}

📊 二、可视化拆分流程

假设旧容量是 8(oldCap = 8),扩容后为 16:

旧索引(index = 3)处的链表:A(hash=3), B(hash=11), C(hash=19), D(hash=27)哈希值(二进制):A = 0000_0011B = 0000_1011C = 0001_0011D = 0001_1011oldCap = 0000_1000位运算: hash & oldCapA: 3  & 8  = 0  → 继续放在索引 3B: 11 & 8  = 8  → 放在新索引 3 + 8 = 11C: 19 & 8  = 0  → 索引 3D: 27 & 8  = 8  → 索引 11结果如下:
新数组:index 3:  A → Cindex11:  B → D

📌 结论:

  • 拆分后每个链表最多分成 2 段;
  • 不需要重新计算 hash,只需根据一位是否为 1 判断目标桶;
  • 效率极高,是扩容过程的核心优化。

💡 三、Java 代码模拟链表拆分

public class HashMapSplitDemo {static class Node {int hash;String key;Node next;Node(int hash, String key, Node next) {this.hash = hash;this.key = key;this.next = next;}}public static void main(String[] args) {int oldCap = 8;Node head = new Node(3, "A",new Node(11, "B",new Node(19, "C",new Node(27, "D", null))));Node loHead = null, loTail = null;Node hiHead = null, hiTail = null;for (Node e = head; e != null; e = e.next) {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;}}// 断尾if (loTail != null) loTail.next = null;if (hiTail != null) hiTail.next = null;System.out.print("原地链表: ");printList(loHead);System.out.print("迁移链表: ");printList(hiHead);}private static void printList(Node head) {Node p = head;while (p != null) {System.out.print(p.key + "(" + p.hash + ") → ");p = p.next;}System.out.println("null");}
}

🧪 输出示例:

原地链表: A(3) → C(19) → null
迁移链表: B(11) → D(27) → null

刚好和我们前面图示的拆分一致 ✅


✅ 总结:链表拆分的意义

优点描述
效率高只用一次位运算,无需重新计算 hash
减少冲突重新分布节点,有助于均衡哈希桶分布
内存复用节点对象不重建,仅更换位置

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com