ArrayList扩容在add前检查size+1是否超限,非满才扩;首次add触发扩容至10,后续按1.5倍增长,可预分配避免性能损耗。

扩容触发时机:不是“满了才扩”,而是“要放不下时就扩”
ArrayList 的扩容不是等 elementData 数组真被填满才动手,而是在每次调用 add(E) 前,先检查:当前 size + 1 是否超过现有容量。这个判断发生在 ensureCapacityInternal(size + 1) 中。
- 如果是无参构造(
new ArrayList()),首次add()时elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(一个空数组),此时minCapacity被设为Math.max(10, 1) = 10,直接触发扩容到 10 - 第 2 到第 10 个元素:容量够用,不扩容
- 第 11 个元素:
size + 1 == 11 > 10,触发扩容
常见错误现象:以为“加到第 10 个才扩容”,结果在循环里反复 add() 却没预估容量,导致第 11、16、23……次添加都伴随一次 Arrays.copyOf(),性能陡降。
扩容计算逻辑:1.5 倍不是魔法,是位运算的取舍
新容量公式是:int newCapacity = oldCapacity + (oldCapacity >> 1)
右移一位 ≈ 除以 2,所以本质就是 oldCapacity * 1.5(向下取整)。
- 10 → 15
- 15 → 22(15 + 7)
- 22 → 33(22 + 11)
- 33 → 49(33 + 16)
但注意:这个 1.5 倍只是“默认策略”,不是铁律。当所需最小容量(minCapacity)比算出来的 newCapacity 还大时,会直接采用 minCapacity —— 比如 addAll() 一次性加 100 个元素,而当前容量只有 50,那就会一步到位扩到至少 100,跳过中间多次小扩。
性能影响:位运算快,但 1.5 倍会导致容量永远不是 2 的幂,和 HashMap 的 2 倍扩容相比,内存对齐友好度略低;不过对 ArrayList 来说,连续访问更重要,这点差异可忽略。
内存布局真相:elementData 不是“一直有”,而是“按需加载”
JDK 8+ 的懒加载机制让 elementData 的生命周期更清晰:
-
new ArrayList()→elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA(长度为 0 的共享空数组) - 首次
add()→ 触发grow()→ 分配新数组new Object[10]→elementData指向它 - 后续扩容 →
Arrays.copyOf()创建新数组,旧引用被 GC
容易踩的坑:
- 误以为
elementData.length就是“当前容量”,其实它确实是,但要注意:刚构造完还没 add 时,elementData.length == 0,不是 10 -
trimToSize()可把容量缩回到size,但多数场景没必要——缩容后立刻再增,反而多一次复制 - 多线程环境下直接共享
ArrayList实例,哪怕只读,也可能因扩容中elementData引用被替换,导致某线程看到部分初始化的数组(虽然概率低,但ConcurrentModificationException更常见)
怎么避免扩容拖慢性能:预分配不是“可选优化”,而是高频写场景的刚需
如果你清楚要塞多少数据,不预分配就是在白扔 CPU 和内存带宽。
- 循环前明确知道总数:用
new ArrayList(expectedSize) - 批量添加集合:用
new ArrayList(collection.size()),再addAll() - 构造后立即
ensureCapacity(n)也行,但不如构造时指定直观 - Guava 的
Lists.newArrayListWithCapacity(100)可读性更好,语义明确
反例:
ArrayList<string> list = new ArrayList(); for (int i = 0; i <p>真实项目里,日志聚合、批量导出、分页查询组装结果集——这些地方漏掉初始容量,就是给 GC 和年轻代增加无谓压力。</p> <p>真正容易被忽略的点:扩容不是纯时间问题,而是<strong>内存局部性破坏</strong>。每次 <code>Arrays.copyOf()</code> 都申请一块新连续内存,旧数组待回收,如果频繁扩,会造成堆内存碎片化,尤其在老年代压力大的服务中,可能间接诱发 Full GC。</p></string>










