ensureCapacity 不是扩容操作,而是预检查并按需分配更大底层数组空间;它不改变 size(),仅确保后续写入不频繁触发 O(n) 扩容复制。

为什么 ensureCapacity 不能“手动触发扩容”
它根本不是扩容操作,只是预检查并提前分配底层数组空间——如果当前容量不足,就新建一个更大的数组,把旧元素复制过去;如果够用,什么也不做。很多同学以为调用它就能“强制扩容”,结果发现 size() 没变、capacity() 也没立刻增长(比如在 ArrayList 中),其实是误读了语义。
真正影响写入性能的,是频繁扩容引发的多次数组复制。一次 add() 可能触发扩容,而扩容成本是 O(n),10 万次追加若触发 15 次扩容,光复制就可能多跑几百万次元素移动。
-
ensureCapacity必须在写入前调用,且传入的是「最小所需容量」,不是“想扩多少就扩多少” - 对空
ArrayList,默认初始容量是 10;若你知道要塞 20000 个元素,直接new ArrayList(20000)比反复ensureCapacity更干净 - 调用后不会清空或重置已有数据,但也不会改变
size()—— 它只管底层数组,不管逻辑长度
ensureCapacity 的正确调用时机和参数计算
适用于你明确知道最终规模,且写入是集中、分批、非流式的场景。例如:解析一个已知行数的 CSV 文件,或批量处理数据库查出的 List<Record>。
错误做法是边 add 边 check、每 1000 次调一次 ensureCapacity;正确做法是在循环前一次性算好上限:
int expectedSize = 15000;
List<String> list = new ArrayList<>();
list.ensureCapacity(expectedSize); // ← 这里才起作用
for (int i = 0; i < expectedSize; i++) {
list.add("item-" + i);
}- 参数应 ≥ 预期最大
size(),不要减去当前size()(ensureCapacity接收的是总容量下限) - 如果数据源本身有
size()方法(如Collection、数组),直接用它;否则宁可略高估,也不要低估 - 对
StringBuilder同理:ensureCapacity(1024 * 1024)可避免字符串拼接时反复 realloc,但前提是确定最终字符数
不同容器的 ensureCapacity 行为差异
不是所有集合都有这个方法。只有底层基于数组、且扩容代价显著的类才提供它,典型就是 ArrayList 和 StringBuilder。而 LinkedList、ConcurrentLinkedQueue 或 HashMap 都没有——它们的扩容机制完全不同,或根本不需要连续内存。
-
ArrayList.ensureCapacity(int):调整内部Object[]数组,策略是“至少达到参数值”,实际分配可能更大(如 JDK 17 中会按 1.5 倍增长规则向上取整) -
StringBuilder.ensureCapacity(int):调整内部char[],行为类似,但注意它还受toString()缓存影响,扩容后旧缓存失效 -
Vector也有该方法,但它自带同步开销,大规模写入时通常不如ArrayList+ 预分配高效
容易被忽略的边界和副作用
最常踩的坑,是以为调用 ensureCapacity 就万事大吉,结果发现 GC 压力反而上升,或者并发写入时出现诡异问题。
- 过度预分配浪费内存:申请 100 万容量却只写 10 万个对象,堆里多占几 MB 无用数组,GC pause 时间可能明显增加
- 多线程下不安全:即使你提前
ensureCapacity,多个线程同时add()仍需同步,该方法本身不提供线程保护 - 某些框架封装后屏蔽了该方法:比如 MyBatis 的
ResultHandler回调中返回的List可能是不可变或代理对象,调用ensureCapacity会抛UnsupportedOperationException
真要压榨写入性能,优先级应该是:确认规模 → 构造时指定初始容量 → 避免中间截断/清空/转数组等隐式扩容操作 → 最后才考虑 ensureCapacity 补漏。它是个保险栓,不是加速器。


















