ArrayList

ℹ️定位说明

List 系列第 1 篇详篇(整体认知见 00-List总览)。ArrayList 是日常开发的默认 List 实现:底层动态数组、随机访问 O(1)、扩容 1.5 倍。本篇覆盖常用操作、扩容源码(grow())、删除、序列化与 fail-fast 机制。

概念与本质

ArrayList 是 Java 中最常用的集合类之一,属于 java.util 包,是基于动态数组实现的可变长度顺序表。其特点是:

  • 支持随机访问(O(1)),性能优于链表
  • 插入、删除元素时可能涉及数组拷贝,效率相对较低(O(n))
  • 本质是对数组功能的封装与增强

源码结构:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, Serializable

其中的 RandomAccess 是一个标志性接口,表示支持快速随机访问。

常用功能与用法

创建与初始化

List<String> list1 = new ArrayList<>();                  // 默认容量10
List<Integer> list2 = new ArrayList<>(100);              // 指定初始容量
List<String> list3 = new ArrayList<>(list1);             // 通过已有集合创建

增删改查

list.add("a");                   // 尾部添加
list.add(1, "b");                // 指定位置插入
list.remove("a");                // 删除指定元素
list.remove(1);                  // 删除指定位置
list.set(0, "new");              // 修改元素
String s = list.get(0);          // 获取元素

遍历方式

// 普通 for 循环
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// 增强 for 循环
for (String s : list) {
    System.out.println(s);
}

// 迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

// ListIterator(支持双向遍历)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    System.out.println(lit.next());
}

扩容机制

当你向 ArrayList 添加新元素时,系统会先检查底层数组是否还有空间。比如当前已有 10 个元素,再插入第 11 个,就会发现容量已满。此时就会触发扩容机制,ArrayList 会调用 grow() 方法,将原数组扩容为原来的 1.5 倍(如从 10 扩到 15),然后通过 Arrays.copyOf() 将原数据复制到新数组中,再完成新元素的插入。这个过程虽然对你来说是"自动"的,但背后其实发生了一次数组复制,所以频繁扩容代价很高,建议在构造时就尽可能预估初始容量,避免反复扩容影响性能。

默认容量

ArrayList 底层是基于数组 elementData[] 实现的,默认初始容量为 10:

private static final int DEFAULT_CAPACITY = 10;

在你使用无参构造创建 ArrayList 后,并不会立刻分配大小为 10 的数组,而是懒惰初始化——第一次添加元素时才初始化为默认容量:

ArrayList<String> list = new ArrayList<>(); // 初始时 elementData = {}
list.add("a"); // 第一次添加,初始化为长度 10 的数组

添加元素时的扩容逻辑

每次添加元素,都会调用 ensureCapacityInternal(size + 1) 方法检查容量是否够用:

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);
    }
    ensureExplicitCapacity(minCapacity);
}

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

核心扩容方法 grow()

真正负责扩容的是 grow() 方法:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩为 1.5 倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; // 防止容量还是不够
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity); // 防止超大数组溢出
    elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝到新数组
}
  • oldCapacity >> 1 等价于 oldCapacity / 2
  • newCapacity = oldCapacity + oldCapacity / 2,约为 1.5 倍
  • Arrays.copyOf() 拷贝原数组内容到新数组

注意:拷贝数组是代价很高的操作(O(n)),频繁扩容会严重影响性能。

删除操作

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。

public E remove(int index) {
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // 避免内存泄漏
    return oldValue;
}
  • 删除会造成后续元素整体前移
  • 时间复杂度为 O(n)

序列化机制

ArrayList 为了避免序列化冗余空间、提升效率和数据一致性,通过 transient + 自定义的 writeObject() / readObject() 方法,仅对有效元素进行精准序列化。这是对默认序列化机制的精细化控制,体现了 Java 对容器类序列化的高度优化与兼容性设计。

为什么要特殊处理序列化?

ArrayList 是基于数组实现的,并具有动态扩容特性,比如你初始化了一个容量为 100 的 ArrayList,但实际只放了 3 个元素。此时底层的数组 elementData 仍然有 97 个 null 空位。

如果不处理,直接把整个数组都序列化,会浪费大量空间;更严重的是,可能会暴露未使用或敏感数据。

transient 修饰 elementData

transient Object[] elementData;

关键点:elementDatatransient 修饰的成员变量,表示它不会被 Java 默认序列化机制处理。

这意味着:JVM 不会自动保存这个数组的内容,但我们可以自己手动决定哪些元素需要序列化、如何序列化。

手动控制序列化过程(核心)

ArrayList 实现了 writeObject()readObject() 方法,用来自定义序列化逻辑。这两个方法是 Serializable 接口的"钩子",在序列化或反序列化时由 JVM 自动反射调用。

writeObject() —— 序列化过程

private void writeObject(ObjectOutputStream s) throws IOException {
    int expectedModCount = modCount;
    s.defaultWriteObject();          // 序列化非 transient 字段(如 size)
    s.writeInt(size);                // 再写入当前实际元素个数 size

    // 只序列化已有的元素,忽略空位
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }

    // 结构变更校验,防止并发修改
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

解释:

  • s.defaultWriteObject():序列化默认字段(如 size)
  • writeInt(size):记录有几个元素需要反序列化回来
  • 循环只序列化前 size 个有效元素
  • 最后校验 modCount 是否变化,防止并发修改时状态不一致

readObject() —— 反序列化过程

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    s.defaultReadObject();        // 反序列化非 transient 字段(如 size)
    s.readInt();                  // 读取并丢弃 capacity(只是为了兼容 clone)

    if (size > 0) {
        ensureCapacityInternal(size);      // 确保底层数组够用
        Object[] a = elementData;

        for (int i = 0; i < size; i++) {
            a[i] = s.readObject();         // 恢复元素数据
        }
    }
}

解释:

  • defaultReadObject() 读取默认字段(如 size)
  • readInt() 是为了向前兼容某些版本中的 capacity 记录
  • 如果 size > 0,就初始化 elementData,并填入反序列化的元素

使用示例(序列化和反序列化)

ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");

// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"));
oos.writeObject(list);
oos.close();

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.ser"));
ArrayList<String> result = (ArrayList<String>) ois.readObject();
ois.close();

System.out.println(result); // [apple, banana]

补充说明:为什么使用 modCount?

modCount 是 ArrayList 的一个结构修改计数器,用于检测并发修改(fail-fast)。在 writeObject() 中,序列化前后 modCount 不一致,说明期间被修改过,立刻抛出异常,防止写入脏数据。

Fail-Fast 机制

内部维护一个 modCount 变量,记录结构修改次数(结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化):

protected transient int modCount = 0;

如在迭代时修改结构,modCount 不一致,将抛出 ConcurrentModificationException

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("a")) {
        list.remove("a"); // 会抛异常
    }
}

正确做法应使用迭代器自身的 remove() 方法。

总结特点

特性 说明
底层结构 动态数组(Object[])
查找效率 快速随机访问,O(1)
插入删除效率 插入、删除慢,O(n),尤其是头部和中间位置
线程安全 非线程安全,可用 Collections.synchronizedList() 包装
扩容策略 容量不足时扩为原来的 1.5 倍
迭代方式 for、foreach、iterator、listIterator
典型应用 查找频繁,插入删除较少的场景,如缓存、索引表等

⬅️ 00-List总览 🏠 00-Java ➡️ LinkedList