个人网站

你天天用 ArrayList,但面试官一问"ArrayList 怎么扩容的",你就只能说"每次扩大 1.5 倍"——再追问"1.5 倍怎么算的"、“扩容时数组怎么搬”、“初始容量是多少”,就答不上来了。

今天咱们把 ArrayList 的扩容机制从底层源码讲透。

一、先说结论:ArrayList 扩容核心事实

维度 说明
底层结构 Object[] 数组
默认初始容量 0(首次 add 扩为 10)
扩容触发 元素个数 > 数组长度时
扩容比例 新容量 = 旧容量 × 1.5(右移 1 位)
扩容操作 Arrays.copyOf() 复制到新数组
时间复杂度 均摊 O(1),单次扩容 O(n)

一句话记住:ArrayList 扩容像搬家——房子住不下了,找个 1.5 倍大的新房子,把东西搬过去。

二、初始容量:不是你以为的 10

// JDK 8+ 源码
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  // 👈 空数组!不是 10
}

无参构造时,内部数组是空的,容量是 0。 那什么时候变成 10?首次 add 时!

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 检查是否需要扩容
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);  // 👈 首次 add,至少扩到 10
    }
    ensureExplicitCapacity(minCapacity);
}

生活类比: ArrayList 像懒人租房——先不租,等要住的时候再租一个 10 间的。

构造方式 初始容量
new ArrayList() 0(首次 add → 10)
new ArrayList(20) 20
new ArrayList(otherList) otherList.size()

三、扩容核心:1.5 倍是怎么算的

当元素个数超过数组长度,触发扩容:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 👈 右移 1 位 = 除以 2 → 1.5 倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;  // 1.5 倍还不够,直接用需要的容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);  // 超大容量处理
    elementData = Arrays.copyOf(elementData, newCapacity);  // 👈 搬家!
}

1.5 倍的数学: oldCapacity + (oldCapacity >> 1) = old + old/2 = 1.5 × old

为什么用位运算? 右移 1 位比除以 2 快,这是底层代码的性能优化。

扩容流程:

1. 计算新容量 = 旧容量 × 1.5
2. 如果 1.5 倍还不够(如 addAll 大量元素),直接用所需容量
3. 创建新数组
4. Arrays.copyOf 复制旧数据到新数组 👈 这步 O(n)
5. elementData 指向新数组

生活类比: 搬家流程——算新房子要多大(1.5 倍),租新房子,把旧家具搬过去,旧房子退租。

四、扩容的性能代价

单次扩容 O(n): 需要复制整个数组。

// 模拟:连续 add 100 万个元素
ArrayList<Integer> list = new ArrayList<>();  // 初始容量 0
// 扩容次数:10 → 15 → 22 → 33 → ... → 大约 24 次
// 每次扩容都要复制整个数组

但均摊 O(1): 把扩容代价分摊到每次 add,平均每次 add 只需 O(1)。

假设当前容量 C,add 一个元素触发扩容:
- 复制 C 个元素:O(C)
- 接下来可以 add C/2 个元素不需要扩容
- 均摊:O(C) / (C/2) = O(1)

优化建议: 如果预知元素数量,指定初始容量避免多次扩容:

// ✅ 已知要放 10000 个元素
ArrayList<Integer> list = new ArrayList<>(10000);  // 一次到位

// ❌ 默认初始容量,多次扩容
ArrayList<Integer> list = new ArrayList<>();  // 扩容约 17 次

五、和 Vector 扩容的区别

维度 ArrayList Vector
扩容比例 1.5 倍 2 倍
线程安全 ✅(synchronized)
扩容增量 0(不可自定义) 可通过 capacityIncrement 自定义
// Vector 的 grow
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
    capacityIncrement : oldCapacity);  // 👈 默认 2 倍

Vector 扩 2 倍更浪费空间,ArrayList 扩 1.5 倍更节省。 这也是 ArrayList 取代 Vector 的原因之一。

ArrayList 自动扩容 全景

ArrayList 自动扩容 全景

初始容量
├── 无参构造 → 0(首次 add → 10)
├── 指定容量 → 指定值
└── 传入集合 → 集合 size

扩容机制
├── 触发条件 ── size + 1 > elementData.length
├── 扩容比例 ── 1.5 倍(oldCapacity >> 1)
├── 兜底处理 ── 1.5 倍不够则直接用 minCapacity
├── 搬家操作 ── Arrays.copyOf()
└── 均摊复杂度 ── O(1)

与 Vector 对比
├── 扩容比例 ── 1.5 倍 vs 2 倍
├── 线程安全 ── ❌ vs ✅
└── 空间浪费 ── 更少 vs 更多

口诀:首次 add 容量十,一点五倍来扩容,
      右移一位除以二,Arrays.copyOf 把家搬,
      预知大小先指定,省得扩容费功夫。

回答技巧与点评

标准回答

ArrayList 底层基于 Object[] 数组实现,默认初始容量为 0,首次 add 时扩容为 10。当元素个数超过数组长度时触发扩容,新容量为旧容量的 1.5 倍(通过 oldCapacity + (oldCapacity >> 1) 计算)。如果 1.5 倍仍不够(如 addAll 大量元素),则直接使用所需容量。扩容通过 Arrays.copyOf() 将旧数组复制到新数组,单次扩容 O(n),但均摊到每次 add 为 O(1)。建议预知元素数量时指定初始容量,避免多次扩容。

加分回答
  1. 1.5 倍 vs 2 倍的权衡:ArrayList 选择 1.5 倍而不是 2 倍,是空间和时间的权衡。2 倍扩容能保证均摊 O(1),但浪费更多空间;1.5 倍虽然也能均摊 O(1),但空间利用率更高。实际上 1.5 倍还有一个好处:通过 GC 回收的旧数组内存可以被复用——因为新数组不会永远比旧数组大太多
  2. 懒初始化的设计:JDK 8 把无参构造的初始容量从 10 改为 0(懒初始化),是为了节省空 ArrayList 的内存。很多 ArrayList 创建后从未使用,空数组比 10 长度数组省内存。这是一个典型的"延迟初始化"优化
  3. SubList 的坑:ArrayList.subList() 返回的是原列表的视图,不是新列表。对 SubList 的修改会直接影响原列表,扩容时也会互相影响。如果原列表结构被修改(如 add),SubList 会抛 ConcurrentModificationException
面试官点评

这道题考的是你对集合底层实现的了解。能说出"1.5 倍扩容、Arrays.copyOf"是基本要求,能讲清楚初始容量为 0 的懒初始化、1.5 倍的位运算实现、均摊 O(1) 的分析,才算及格。如果你能从设计角度分析 1.5 倍 vs 2 倍的权衡、懒初始化的优化动机,面试官会认为你不只是背了源码,还理解了设计背后的思考。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

更多推荐