首页>Java>正文

ArrayList 扩容机制源码级拆解:1.5 倍、首次变 10 与预分配调优

Feng 56 阅读 Java

ArrayList 的自动扩容是 Java 集合里被问得最多、也最容易答错的一段源码。触发点在哪、新容量怎么算、代价多大,直接决定你会不会在批量写入时踩坑。

下面从 add 入口一路拆到 grow(),把容量推演、构造差异和调优手段讲清楚。

扩容链路总览

一次 add 的完整路径是这样的:

调用 add 方法
  ↓
size + 1 > elementData.length?
  ├─ 否 → 直接添加元素
  └─ 是 → 触发 grow 扩容
              ↓
        旧容量 > 0?
        ├─ 否 → 新容量 = DEFAULT_CAPACITY (10)
        └─ 是 → 新容量 = 旧容量 + 旧容量 >> 1 (即1.5倍)
                    ↓
              新容量仍小于所需最小容量?
              ├─ 否 → 保持新容量
              └─ 是 → 新容量 = 所需最小容量
                          ↓
                    Arrays.copyOf 复制数组
                          ↓
                    新容量 > MAX_ARRAY_SIZE?
                    ├─ 否 → 使用新容量
                    └─ 是 → hugeCapacity 处理 (上限 Integer.MAX_VALUE)

记住三个关键节点:add 判断容量、ensureCapacityInternal 层层传递、grow 真正复制数组。

入口与容量确保

add(E e) 是扩容的唯一常见入口:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

它把 size+1 作为最小需求容量往下传,最终落到 ensureExplicitCapacity:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这里有个坑:minCapacity 是需求值,不是最终容量。真正算多大由 grow 决定。

grow 的容量计算

核心只有几行:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

newCapacity = oldCapacity + (oldCapacity >> 1),右移一位等价于除以 2,所以新容量就是旧容量的 1.5 倍。

容量上限的处理单独拎出来:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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

超过 MAX_ARRAY_SIZE 会走 hugeCapacity,逼近 Integer.MAX_VALUE 时可能直接 OutOfMemoryError。

1.5 倍是怎么滚起来的

计算方式很直白:

int newCapacity = oldCapacity + (oldCapacity >> 1);

逐次推演一遍,数字比描述更有说服力:

扩容次数 当前容量 计算过程 新容量
初始化 0(空数组) 首次 add → max(10,1) 10
第1次 10 10 + 10>>1 15
第2次 15 15 + 7 22
第3次 22 22 + 11 33
第4次 33 33 + 16 49
第5次 49 49 + 24 73
第6次 73 73 + 36 109

注意 15 变 22 这一步:15>>1 是 7 不是 7.5,整数右移直接截断,所以实际倍率略低于 1.5。

为什么不是 2 倍或 1.2 倍

倍数 优点 缺点
2 倍 扩容次数少 内存浪费严重(可能浪费 50%)
1.5 倍 平衡性能和内存 扩容次数略多于 2 倍
1.2 倍 内存更节省 扩容频繁,复制开销大

1.5 倍是时间和空间的折中。另外 1.5 倍还有个隐含好处:旧数组空间更容易被后续分配复用,不像 2 倍那样每次都要一块全新的连续内存。

三种构造方法的初始容量差异

无参构造:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

初始是空数组,不是容量 10。容量 10 是首次 add 时才给的,这点面试高频。

指定初始容量:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException();
    }
}

对比一下:

构造方式 初始数组 首次 add 后容量 适用场景
new ArrayList() 空数组 {} 10 元素数量未知
new ArrayList(0) 空数组 {} 1(按需) 确定元素很少
new ArrayList(100) 容量 100 数组 100 已知元素数量(推荐)

文章配图

扩容代价与预分配

扩容的本质是 Arrays.copyOf,一次 O(n) 的数组复制。默认从 10 开始添加 1000 万元素,大约要经历 34 次扩容,累计复制量相当可观。

预分配容量 1000 万后,扩容次数为 0,省掉的全是复制开销。

最佳实践就是提前给够:

// 不推荐:频繁扩容
List<String> bad = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    bad.add("item" + i);
}

// 推荐:预分配容量
List<String> good = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
    good.add("item" + i);
}

// 或先创建再显式扩容
List<String> list = new ArrayList<>();
list.ensureCapacity(1000000);
for (int i = 0; i < 1000000; i++) {
    list.add("item" + i);
}

如果容量是逐步确定的,用 ensureCapacity 手动补:

public void ensureCapacity(int minCapacity) {
    if (minCapacity > elementData.length 
        && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 
             && minCapacity <= DEFAULT_CAPACITY)) {
        modCount++;
        grow(minCapacity);
    }
}

旧数组、缩容与批量添加

扩容后旧数组失去引用,交给 GC 回收:

elementData = Arrays.copyOf(elementData, newCapacity);

大量删除后想释放多余内存,用 trimToSize:

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

它把底层数组缩到和 size 一样大,属于主动缩容:

ArrayList<String> list = new ArrayList<>(1000000);
// ... 大量删除操作后
list.trimToSize();

批量添加时,addAll 会先算好总需求一次性扩容,避免多次小批量触发复制:

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

几个高频疑问

Q1:最大容量是多少?理论上是 Integer.MAX_VALUE,实际实现里是 Integer.MAX_VALUE – 8:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

减 8 是因为部分 JVM 会在数组头存对象信息,超限可能抛 OutOfMemoryError。

Q2:size() 和数组长度为什么不同?elementData.length 是物理容量,size 是逻辑元素个数,恒有 size <= elementData.length。

Q3:扩容为什么改 modCount?虽然 size 没变,但底层数组结构变了,属于结构性修改,用于 fail-fast:

int expectedModCount = modCount;

Q4:和 Vector 的区别?ArrayList 扩容 1.5 倍、非线程安全;Vector 扩容 2 倍(可指定增量)、方法带 synchronized,默认容量同为 10。

核心要点

要点 内容
触发条件 size + 1 > elementData.length
扩容倍数 1.5 倍(oldCapacity + (oldCapacity >> 1))
首次扩容 空数组 → 容量 10
核心方法 grow() → Arrays.copyOf()
时间复杂度 扩容 O(n),均摊 O(1)
最佳实践 预分配容量(已知大小时)
最大容量 Integer.MAX_VALUE – 8
缩容方法 trimToSize()

一句话记忆:扩容 1.5 倍,首次添加变 10;预分配省复制,性能优化记心中。

代码口诀:

List<String> list1 = new ArrayList<>();

List<String> list2 = new ArrayList<>(10000);

list2.ensureCapacity(20000);

Feng
这位作者很神秘,还没有填写简介。