扩容触发的精确条件是add(E e)中判断size + 1 > elementData.length时立即触发;新容量先按1.5倍(oldCapacity + oldCapacity>>1)计算,不足则取所需最小值;扩容必全量复制元素,时间复杂度O(n);非线程安全因扩容含计算、创建、复制三步非原子操作。

Java 中 ArrayList 的扩容机制看似简单,但大厂面试常从底层实现、边界场景、性能影响三方面深挖细节。光说“1.5倍扩容”远远不够,关键要讲清 触发时机、计算逻辑、数组复制开销、线程安全性 这四个硬核点。
扩容触发的精确条件是什么?
不是“满了才扩”,而是在 add(E e) 方法中,判断 size + 1 > elementData.length 时立即触发。注意两点:
- size 是当前元素个数,elementData.length 是底层数组容量,两者不相等很常见(比如 clear() 后 size=0,但数组没缩容)
- 即使你用 ensureCapacity(int minCapacity) 预留空间,只要 add 时仍越界,照样扩容——ensureCapacity 只是“建议”,不强制改变当前行为
新容量怎么算?1.5 倍不是简单乘法
核心逻辑在 grow() 方法里,分两步:
- 先尝试 newCapacity = oldCapacity + (oldCapacity >> 1),即 old * 1.5(位运算更高效)
- 但若 newCapacity 仍小于所需最小容量(minCapacity),就直接设为 minCapacity;如果 minCapacity 已超 VM 堆限制,会抛 OutOfMemoryError
例如:初始容量 10,add 第 11 个元素 → 计算得 15;若此时要一次性 addAll 20 个元素(minCapacity=30),则 newCapacity 直接取 30,跳过 1.5 倍逻辑。
扩容一定拷贝全部元素?有没有优化空间?
是的,每次扩容都调用 Arrays.copyOf(elementData, newCapacity),本质是 System.arraycopy —— 底层 C 实现,效率高但仍是 O(n) 操作。
- 没有“懒拷贝”或“写时复制”,扩容即全量迁移
- 频繁 add 导致多次扩容(如从 10→15→22→33…),会产生冗余数组对象,GC 压力增大
- 应对方案:预估大小,构造时指定 initialCapacity;或用 LinkedList 处理高频插入(但牺牲随机访问)
为什么 ArrayList 不是线程安全的?扩容过程暴露了什么问题?
扩容本身不是原子操作,包含三步:计算新容量 → 创建新数组 → 复制元素。多线程下可能:
- 线程 A 判断需扩容,刚算出 newCapacity,被挂起
- 线程 B 完成扩容并写入新数组,size 更新为新值
- 线程 A 恢复后仍用旧 oldCapacity 创建数组,再复制——导致数据覆盖、丢失或 ArrayIndexOutOfBoundsException
所以即使只读操作,若同时有 add,仍需外部同步(如 Collections.synchronizedList 或 CopyOnWriteArrayList)。
不复杂但容易忽略:扩容是 ArrayList 性能拐点,理解它等于抓住了动态数组设计的本质权衡——空间换时间,以及 Java 集合类“不承诺线程安全”的底层哲学。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











