预估元素个数 n,初始容量应设为 tablesizefor((int) math.ceil(n / 0.75)),即按负载因子 0.75 反推最小理论容量再向上取整到 2 的幂;例如 n=1000 时应设 1334,得实际容量 2048,避免提前扩容。

HashMap 初始化容量怎么算才不浪费内存又不触发扩容
直接说结论:预估元素个数 n,就设初始容量为 tableSizeFor(n * 2) 对应的最小 2 的幂(即向上取整到最近的 2 的幂),而不是简单写 n 或 n + 1。
原因很简单:HashMap 底层数组长度必须是 2 的幂,构造时传入的 initialCapacity 会被 tableSizeFor() 自动“掰正”。比如你传 10,实际初始化容量是 16;传 17,结果是 32。但如果你没考虑负载因子,光按元素数设,大概率会多一次扩容。
- 默认负载因子是
0.75,意味着容量为16时,最多存12个元素才不扩容 - 所以真要塞
n个元素,理论最小容量是n / 0.75,再向上取整到 2 的幂 - Java 里这一步由
tableSizeFor()完成,你只需要传入(int) Math.ceil(n / 0.75)即可 - 示例:预估放 1000 个键值对 →
1000 / 0.75 ≈ 1333.33→ 向上取整为1334→tableSizeFor(1334) = 2048
为什么 new HashMap(1000) 还是会扩容一次
因为 new HashMap(1000) 中的 1000 是你“希望”的初始容量,但 HashMap 构造函数内部会把它喂给 tableSizeFor(),而 tableSizeFor(1000) 返回的是 1024 —— 这个容量在负载因子 0.75 下只能安全存 768 个元素。
一旦你 put 第 769 个元素,就会触发 resize,数组从 1024 扩到 2048,所有已有元素 rehash —— 白花了 CPU 和 GC 压力。
立即学习“Java免费学习笔记(深入)”;
- 错误写法:
new HashMap(1000)→ 实际容量1024→ 容量上限768 - 正确写法:
new HashMap((int) Math.ceil(1000 / 0.75))→ 传1334→tableSizeFor(1334) = 2048 - 注意:
Math.ceil()返回double,必须强转int,否则编译不过 - 如果用的是 Java 19+,可以考虑
Map.ofEntries()配合不可变集合,避开这个问题
负载因子改小或改大有什么实际影响
负载因子不是调优银弹,改它是在空间和时间之间做显式权衡:调小更省内存但更慢;调大更省时间但更费内存,且哈希冲突概率明显上升。
- 设成
0.5:容量翻倍,冲突减少,但一半内存空着;适合 key 冲突敏感、内存宽裕的场景(如缓存索引) - 设成
0.9:少占内存,但 get/put 平均链表长度变长,极端情况下退化成 O(n);仅建议在只读、key 分布极均匀、且明确压测过性能的场景 -
new HashMap(1000, 0.5f)实际容量仍是tableSizeFor(1000 / 0.5) = tableSizeFor(2000) = 2048,不是1000 - 别碰
static final float DEFAULT_LOAD_FACTOR = 0.75f的源码 —— 这个值是经过大量实测平衡出来的,不是拍脑袋定的
ConcurrentHashMap 和 LinkedHashMap 的初始化逻辑一样吗
不一样。虽然都继承自 AbstractMap,但初始化行为差异很大。
-
ConcurrentHashMap不接受初始容量参数(构造函数里没有int initialCapacity),它用的是分段控制 + 动态扩容,传进去的initialCapacity只是“预估总大小”,内部会按并发线程数和该值共同决定起始分段数,不是数组长度 -
LinkedHashMap的构造函数签名和HashMap一致,初始化逻辑也一致,但多了维护插入顺序的双向链表开销,相同容量下内存占用略高 - 如果你需要有序 + 预估容量,用
LinkedHashMap(int initialCapacity, float loadFactor)没问题,但别指望它比HashMap更快 - 别试图通过
new LinkedHashMap(1000, 0.75f, true)(访问顺序)来优化 LRU 缓存——它不会自动 trim,得自己重写removeEldestEntry()
最常被忽略的一点:初始化容量只影响第一次建表,后续所有扩容都是 2 倍增长,且 rehash 成本随 size 增大非线性上升。宁可稍微多估一点,也别卡着边界设。








