ArrayList是基于Object[]的动态数组,支持O(1)随机访问,中间删除为O(n),扩容按1.5倍增长但有溢出风险,序列化仅保存有效元素,迭代器通过modCount实现fail-fast机制。

ArrayList底层用Object[]数组存储元素
ArrayList不是链表,也不是哈希结构,它本质就是一个封装好的动态数组。内部核心字段是transient Object[] elementData,所有元素都连续存放在这个数组里。插入、删除、随机访问都依赖数组的内存连续性,所以get(int index)是O(1),而remove(int index)在中间位置删除时需移动后续元素,最坏O(n)。
注意elementData被标记为transient,是因为它可能包含null占位或未用空间,序列化时不希望把整个冗余数组都保存下来——实际序列化逻辑由writeObject方法控制,只写入size个有效元素。
扩容机制:1.5倍增长但有整数溢出风险
当add(E e)发现size == elementData.length时触发扩容。新容量计算逻辑是:newCapacity = oldCapacity + (oldCapacity >> 1),也就是1.5倍。但这个算法在大数组下容易溢出:
- 若当前容量是
Integer.MAX_VALUE - 8(这是Arrays.copyOf能支持的最大安全长度),再加一半就会超过Integer.MAX_VALUE - 此时会进入兜底逻辑:
newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE,其中MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
所以你可能会看到扩容后容量突然变成2147483647,接着下一次add就抛OutOfMemoryError: Requested array size exceeds VM limit——不是内存不够,而是JVM禁止分配超过Integer.MAX_VALUE - 8长度的数组。
立即学习“Java免费学习笔记(深入)”;
add与add(int index, E element)性能差异明显
看似都是add,行为完全不同:
-
add(E e)追加到末尾,均摊O(1),仅在扩容时有额外开销 -
add(int index, E element)需要先校验索引范围,再将index及之后所有元素右移一位,时间复杂度O(n - index),越靠前越慢
比如在10万元素的ArrayList开头插入一个元素,要移动全部10万个引用。如果业务需要频繁首部插入,应换用LinkedList或考虑ArrayDeque(但注意它不支持随机访问)。
fail-fast迭代器与modCount的作用
ArrayList的iterator()返回的是Itr内部类,它在创建时记录当前modCount值。每次调用next()或remove()前,都会检查modCount是否被外部修改过(比如另一个线程或同一线程中用list.remove()而非iterator.remove())。一旦不一致,立刻抛ConcurrentModificationException。
这不是线程安全保证,只是快速失败机制。它无法防止并发修改,也不能替代同步;它只确保单线程下“遍历时结构没被意外改过”。如果你需要并发安全的动态数组,得用CopyOnWriteArrayList,但它写操作代价极高,读多写少才适用。
public class ArrayListSourceDemo {
public static void main(String[] args) {
ArrayList list = new ArrayList<>(2);
list.add("a");
list.add("b"); // 此时 elementData.length == 2, size == 2
list.add("c"); // 触发扩容:2 + (2 >> 1) == 3 → 新数组长度为3
System.out.println(list.size()); // 3
System.out.println(list.toArray().length); // 3
}
} 真正容易被忽略的是:扩容不是按需精确分配,而是预留增长空间;而modCount的检测粒度是“结构性修改”,哪怕只是set(int index, E element)这种不改变大小的操作,也不会触发fail-fast——它只对add、remove、clear等改变size或数组结构的方法计数。










