Skip to content

ArrayList 底层实现:动态数组的秘密

核心结构:一句话概括

ArrayList 就是一个会自己扩容的数组。它内部维护了一个 Object[],当空间不够时,自动扩容。

java
public class ArrayList<E> {
    // 实际存储元素的数组
    transient Object[] elementData;

    // 实际元素个数
    private int size;
}

就这么简单。elementData 就是那个会不断扩容的数组,size 记录当前有多少个元素。


内部结构图

ArrayList 内部就像一个「会变长的盒子」:

┌─────────────────────────────────────────────────────┐
│ elementData(Object[] 数组)                          │
│ ┌───────┬───────┬───────┬───────┬───────┬───────┐   │
│ │  [0]  │  [1]  │  [2]  │  [3]  │  [4]  │  ...  │   │
│ ├───────┼───────┼───────┼───────┼───────┼───────┤   │
│ │  "A"  │  "B"  │  "C"  │  "D"  │  "E"  │ null  │   │
│ └───────┴───────┴───────┴───────┴───────┴───────┘   │
│     0       1       2       3       4      5        │
│                                                     │
│ size = 5(实际元素个数)                              │
│ elementData.length = 10(数组容量)                    │
└─────────────────────────────────────────────────────┘

构造方法:3 种创建方式

java
// 1. 默认构造 → 空数组,首次添加才分配 10
ArrayList<String> list1 = new ArrayList<>();

// 2. 指定初始容量 → 预分配,避免扩容
ArrayList<String> list2 = new ArrayList<>(100);

// 3. 从已有集合创建
ArrayList<String> list3 = new ArrayList<>(Arrays.asList("A", "B", "C"));

默认容量是 0,不是 10

注意:默认构造方法创建的 ArrayList 内部数组是空的。第一次 add() 时才会分配容量 10。这是为了节省内存——如果你创建了 100 个 ArrayList 但都没放元素,就不会占用 100 × 10 = 1000 个对象的空间。

java
// 源码逻辑(简化)
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组,容量 0
}

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

添加元素:两步走

添加到末尾

java
public boolean add(E e) {
    // 1. 确保容量够用(扩容逻辑在 ensureCapacityInternal 里)
    ensureCapacityInternal(size + 1);
    // 2. 放入数组,size++
    elementData[size++] = e;
    return true;
}

添加到指定位置

java
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);
    // 将 index 及之后的元素向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

图示:

add(2, "X") 到 [A, B, C, D, E]:

操作前:
┌───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┘
    0   1   2   3   4

System.arraycopy(elementData, 2, elementData, 3, 3)
C, D, E 向后移动一位:

操作后:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ X │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
    0   1   2   3   4   5

扩容机制:容量翻倍的秘密

ArrayList 容量不够时,会自动扩容。扩容公式:新容量 = 旧容量 + 旧容量 / 2 = 旧容量 × 1.5

java
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新容量 = 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 还不够?直接用 minCapacity
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }

    // 超过最大容量?用 Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity);
    }

    // 创建新数组,复制元素
    elementData = Arrays.copyOf(elementData, newCapacity);
}

容量增长轨迹:

容量 0 → 首次添加 → 10
容量 10 → 添加第 11 个 → 15
容量 15 → 添加第 16 个 → 22
容量 22 → 添加第 23 个 → 33
容量 33 → 添加第 34 个 → 49
...

扩容是性能杀手

扩容需要:

  1. 分配新数组
  2. 复制所有元素

如果你知道大概要存多少元素,一开始就指定容量,避免扩容:

java
// ❌ 可能触发多次扩容
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    list.add("item" + i);
}

// ✅ 一步到位
ArrayList<String> list = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
    list.add("item" + i);
}

随机访问:O(1) 的秘密

ArrayList 随机访问极快,因为是直接用数组下标:

java
public E get(int index) {
    rangeCheck(index);
    return elementData[index]; // 直接返回,就是这么快
}

这就是为什么 ArrayList 比 LinkedList 随机访问快 100 倍以上——LinkedList 需要从头或从尾遍历到目标位置。


删除元素:也要移动数组

java
public E remove(int index) {
    rangeCheck(index);

    E oldValue = elementData(index);
    int numMoved = size - index - 1;

    if (numMoved > 0) {
        // 将 index+1 及之后的元素向前移动一位
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    elementData[--size] = null; // 帮助 GC

    return oldValue;
}

删除中间的图示:

remove(2) 从 [A, B, C, D, E]:

操作前:
┌───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┘
    0   1   2   3   4

System.arraycopy(elementData, 3, elementData, 2, 2)
D, E 向前移动一位:

操作后:
┌───┬───┬───┬───┬───┐
│ A │ B │ D │ E │null│
└───┴───┴───┴───┴───┘
    0   1   2   3   4

性能总结

操作时间复杂度说明
随机访问 get(i)O(1)直接数组下标
尾部添加 add(e)均摊 O(1)大多数是 O(1),扩容时 O(n)
中间插入 add(i, e)O(n)需要移动元素
按索引删除 remove(i)O(n)需要移动元素
按对象删除 remove(Object)O(n)查找 + 移动
containsO(n)需要遍历
clearO(n)需要逐个清空引用

线程安全:非线程安全

ArrayList 是非线程安全的。

java
// ❌ 多线程不安全
ArrayList<String> list = new ArrayList<>();
// 线程 A: list.add("A");
// 线程 B: list.add("B");
// 结果:可能只添加了一个元素,或者 size 不正确

// ✅ 线程安全的替代方案
List<String> safe1 = Collections.synchronizedList(new ArrayList<>());
List<String> safe2 = new CopyOnWriteArrayList<>(); // 读多写少场景

总结口诀

ArrayList = 动态数组。随机访问最快,尾部操作高效,中间增删要移动元素。预知容量能省扩容,线程不安全是天生缺陷。


相关链接

基于 VitePress 构建