一、ArrayList 基础认知
ArrayList 是非线程安全 的动态数组,底层实现为Object[]数组,它的核心优势是支持随机访问、查询效率极高,同时封装了自动化的容量管理逻辑,屏蔽了数组固定长度的弊端。
重点:JDK 7 及之后,ArrayList 采用懒加载机制,初始化时不会直接分配容量,只有真正添加元素时,才会触发容量分配。
二、什么是自动扩容?
我们先明确核心定义:自动扩容是 ArrayList 的核心能力:
- 初始化时创建空数组,不占用实际内存空间;
- 执行元素添加操作时,正式分配内存容量;
- 当添加元素导致现有容量不足时,底层会自动创建一个更大的新数组,将原数组元素复制到新数组中,完成扩容。
简单来说:数组长度固定不可变,ArrayList 通过「创建新数组 + 复制元素」模拟了动态扩容效果。
三、ArrayList 扩容全流程(核心源码逻辑)
我们以 JDK 8 为基准,拆解扩容的每一步执行逻辑,这也是面试高频考点。
1. 扩容触发时机
执行add()/addAll()方法添加元素时,会触发容量检查 :判断条件:当前元素个数 + 1 > 数组现有容量
- 条件不成立:直接添加元素
- 条件成立:立即触发扩容流程
2. 核心扩容步骤
(1)懒加载初始化
无参构造创建 ArrayList:new ArrayList()底层数组:private transient Object[] elementData = {};(空数组)第一次添加元素 时,直接将容量扩容为默认值 10。
(2)计算新容量
ArrayList 默认扩容规则:新容量 = 旧容量 + 旧容量 >> 1 → 扩容为原容量的 1.5 倍 >> 1是右移一位,等价于除以 2,运算效率远高于除法。
(3)严格的容量校验
扩容不是无限的,JDK 做了双层安全校验:
- 如果 1.5 倍后的新容量 < 最小需要容量(如批量添加大量元素),直接以「最小需要容量」作为新容量;
- 如果新容量超过
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,则直接使用Integer.MAX_VALUE(2^31-1)作为最终容量。
关键疑问解答:① 为什么是
Integer.MAX_VALUE - 8?部分虚拟机需要在数组对象头中存储数组长度信息,预留 8 字节空间,避免内存溢出(OOM),这是推荐安全上限 。② 为什么最终可以用Integer.MAX_VALUE?-8是安全建议,不是物理上限,当容量不足时,JDK 会用 int 类型的最大值做兜底保障。
(4)数组复制完成扩容
底层通过Arrays.copyOf()方法实现:
- 创建新容量的数组;
- 将原数组元素浅拷贝到新数组;
- 将
elementData指向新数组,丢弃旧数组。
性能瓶颈:数组拷贝是耗时操作,频繁扩容会大幅降低 ArrayList 性能。
四、完整扩容流程总结
- 初始化:空数组,无容量分配;
- 首次添加:容量直接变为 10;
- 常规添加:检查容量,不足则扩容 1.5 倍;
- 容量校准:优先满足最小需求,再限制最大容量;
- 数组复制:完成扩容,替换底层数组。
五、ArrayList 扩容实战优化(开发必备)
扩容的核心损耗是数组复制 ,想要提升性能,核心思路:减少扩容次数。
优化方案 1:指定初始容量(最常用)
如果提前知道元素数量,创建 ArrayList 时直接指定容量,从根源避免扩容:
// 坏代码:未知会触发多次扩容
List<String> list = new ArrayList<>();
// 好代码:提前指定1000容量,零扩容
List<String> list = new ArrayList<>(1000);
优化方案 2:批量添加时手动扩容
添加大量元素前,手动调用ensureCapacity(int minCapacity)方法,一次性扩容到位:
List<String> list = new ArrayList<>();
// 手动扩容,提前分配足够容量
list.ensureCapacity(1000);
// 批量添加元素,无额外扩容
for (int i = 0; i < 1000; i++) {
list.add("测试");
}
优化方案 3:避免中间插入 / 删除
数组结构决定了中间操作会触发元素移动,结合扩容会进一步降低性能,高频增删场景建议使用 LinkedList。