ArrayList 扩容机制

面试官问:"ArrayList 的扩容机制是什么?"

候选人小高答:"ArrayList 默认容量是 10,超过后扩容 1.5 倍。"

面试官追问:"为什么是 1.5 倍,不是 2 倍?"

小高说:"可能 1.5 倍更节省内存?"

面试官又问:"如果一次性 add 100 万个元素,怎么初始化最好?"

小高说不上来。

【面试官心理】 这道题看似简单,但追问"1.5 倍的原因"和"批量初始化"能测出候选人对性能优化的理解。能说出"避免频繁扩容"和"newCapacity 计算公式"的候选人,说明看过源码。

一、ArrayList 底层结构 🔴

// ArrayList 的核心字段
public class ArrayList<E> {
    private static final int DEFAULT_CAPACITY = 10; // 默认容量
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // 存储元素的数组
    private int size;               // 当前元素数量
}

关键点:elementDataObject[],不是泛型数组(因为泛型会被类型擦除)。

二、扩容流程源码解析 🔴

2.1 add 方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 确保容量足够
    elementData[size++] = e;
    return true;
}

2.2 ensureCapacityInternal

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 第一次 add,使用默认容量 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

2.3 扩容的核心:grow 方法

private void grow(int minCapacity) {
    // 旧的容量
    int oldCapacity = elementData.length;

    // 新容量 = 旧容量 + 旧容量/2 = 旧容量 * 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 如果 1.5 倍还不够,直接用 minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 防止溢出
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 核心操作:数组拷贝(创建新数组,复制元素)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

2.4 扩容过程图解

初始状态(无参构造器):
elementData = [] (EMPTY_ELEMENTDATA)
size = 0

第一次 add:
minCapacity = 1
elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA → 触发
minCapacity = Math.max(10, 1) = 10
grow(10):newCapacity = 0 + 0 = 0(第一次特殊处理)

第二次 add:
size = 1, minCapacity = 2
minCapacity < 10,不触发默认容量
grow(2):oldCapacity = 10, newCapacity = 10 + 5 = 15

第十一次 add(触发扩容):
size = 10, minCapacity = 11
grow(11):oldCapacity = 10, newCapacity = 15
elementData = Arrays.copyOf(elementData, 15) // 扩容到 15

三、为什么是 1.5 倍?🔴

3.1 扩容倍数的对比

扩容倍数优点缺点
1.5 倍内存利用率高,扩容不会太频繁扩容次数相对较多
2 倍扩容次数少内存浪费大(如果只需要多一个位置)

3.2 数学分析

JVM 的 Arrays.copyOf() 使用 System.arraycopy(),这是 native 方法,在堆内存中复制数据。

  • 扩容太小:频繁扩容,CPU 开销大
  • 扩容太大:内存浪费

1.5 倍是一个经验最优值

  • 不是 1.0 倍(浪费空间)
  • 不是 2.0 倍(内存浪费过大)
  • 1.5 倍在时间和空间之间取得了平衡
💡

扩容的真正性能瓶颈是 Arrays.copyOf() 的数据复制。如果预估数据量已知,应该预先调用 ensureCapacity() 一次性扩容到位。

四、扩容的性能问题 🔴

4.1 扩容的代价

// 如果你知道最终需要 100 万个元素:
List<String> list = new ArrayList<>(); // 默认容量 10
for (int i = 0; i < 1_000_000; i++) {
    list.add("item"); // 每次 add 都要检查容量
}

// 扩容次数:10 → 15 → 22 → 33 → 49 → 73 → 109 → 163 → 244 → 366 → 549 → 823 → 1234 → 1851 → 2776 → 4164 → 6246 → 9369 → 14053 → 21079 → 31618 → 47427 → 71140 → 106710 → 160065 → 240097 → 360145 → 540217 → 810325 → 1215487
// 大约需要 30 次扩容!每次扩容都要复制已有数据。

4.2 正确的做法

// ✅ 预分配容量
List<String> list = new ArrayList<>(1_000_000);
for (int i = 0; i < 1_000_000; i++) {
    list.add("item"); // 不需要扩容,直接添加
}

// ✅ 或手动确保容量
List<String> list = new ArrayList<>();
((ArrayList<String>) list).ensureCapacity(1_000_000);

五、无参构造器的秘密 🔴

// JDK 6 及之前
public ArrayList() {
    this(10); // 直接分配容量 10
}

// JDK 7 及之后(优化)
public ArrayList() {
    super();
    elementData = EMPTY_ELEMENTDATA; // 空数组,不分配内存!
}

// 第一次 add 时才扩容到 10(懒加载)
// 无参构造器 + 第一次 add
ArrayList<String> list = new ArrayList<>();
// elementData = [](空数组,不占内存)
list.add("hello");
// grow:elementData = Arrays.copyOf([], Math.max(10, 1))
// elementData = [hello](扩容到 10)
💡

JDK 7+ 的无参构造器使用懒加载策略,初始不分配内存。这在创建大量可能为空或很少使用的 ArrayList 时节省了内存。

六、生产避坑

6.1 Integer.MAX_VALUE 边界

// 当容量接近 MAX_ARRAY_SIZE 时
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 留 8 字节

// 如果 minCapacity 超过 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0) {
    newCapacity = hugeCapacity(minCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

6.2 扩容导致的 GC 问题

在高频写入场景(如日志收集),ArrayList 频繁扩容会产生大量临时数组,增加 GC 压力。

解决方案:

  1. 预估容量,一次分配到位
  2. 使用 LinkedList 避免扩容问题(但访问性能差)
  3. 使用 RingBuffer 等无锁队列

【面试官心理】 能说出"懒加载"和"扩容次数计算"的候选人,说明真正研究过 ArrayList 源码。这种追问能力是 P6 的标准要求。