预估元素数需除以负载因子再向上取整,再对齐到2的幂,否则易触发无效扩容;如存800个元素应传1067而非800,得容量2048而非1024。

直接给初始容量不等于真能“装下”预估数量——关键在算对理论最小容量,再对齐到 2 的幂。否则哪怕传了 1000,实际还是 1024 容量,769 个元素就触发扩容,白耗 CPU 和 GC。
别直接用业务数当 initialCapacity
很多人写 new HashMap(800),以为给了 800 就够用。其实 HashMap 会先调用 tableSizeFor(800) → 得到 1024;而默认负载因子 0.75 下,1024 容量最多安全存 768 个元素。第 769 次 put 就强制 resize,所有已有 key 全部 rehash。
- 错误做法:
new HashMap(800)→ 实际容量 1024 → 容量上限 768 - 正确做法:
new HashMap((int) Math.ceil(800 / 0.75))→ 传入 1067 →tableSizeFor(1067) = 2048 - 更稳妥:直接写死
new HashMap(2048, 0.75f),语义清晰,跳过运行时计算
批量构建 Map 时必须预分配
从 List 转 Map 是高频场景,但循环里无预分配会滚雪球式扩容。比如往空 HashMap 插入 1000 个元素,默认容量 16,要经历约 7 次扩容(16→32→64→128→256→512→1024→2048),每次都要遍历迁移全部已存元素。
- 推荐写法:
Map<String, Item> map = new HashMap<>(list.size() + 16),+16 是防边界抖动的缓冲 - Java 10+ 更优:
list.stream().collect(Collectors.toMap(Item::id, Function.identity())),底层已按 size 预估容量 - 避免:
Map map = new HashMap(); for (...) map.put(...)—— 扩容次数不可控
负载因子不是固定值,得看读写比和内存压力
0.75 是通用平衡点,但不是铁律。它本质是时间(冲突链长度)和空间(数组稀疏度)的显式权衡。
- 读多写少、内存宽裕(如配置缓存、枚举映射):可设
0.85f或0.9f,减少扩容频次 - 写入不均匀或 GC 敏感(如 key 是递增字符串):降到
0.6f,压低冲突链平均长度 - 短生命周期 Map(单次请求内创建即弃):可用
new HashMap(16, 1.2f),允许存 19 个再扩容,省掉一次 resize
用 Guava 或 Java 新 API 简化计算
手动算 tableSizeFor((int) Math.ceil(n / 0.75)) 容易出错,尤其强转和取整顺序。有更轻量方案:
- Guava 提供
Maps.newHashMapWithExpectedSize(800),内部自动套用标准公式,一行解决 - Java 19+ 可考虑
Map.ofEntries()构建不可变 Map,彻底避开扩容问题 - ConcurrentHashMap 不同:它按
tableSizeFor((int)(n * 1.5 + 1))计算,预估 100 就传 100,内部会升到 256


















