ArrayList

ArrayList 源码分析

ArrayList 用一块连续的 Object[] 解决「既要数组的 O(1) 随机访问,又要能自动变长」的问题:容量不够时按约 1.5 倍申请新数组并整体拷贝,把扩容代价摊还到每次 add 上。代价是中间增删要搬移元素,且完全不做并发防护,只靠 modCount 提供 fail-fast。

// 基于本地 JDK 源码 (java.base, JDK 21+ 代码线), java.util.ArrayList
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // ...

    private static final int DEFAULT_CAPACITY = 10;    // 默认容量。注意 new ArrayList() 不会立刻分配,延迟到第一次 add

    private static final Object[] EMPTY_ELEMENTDATA = {};    // new ArrayList(0) 共享的空数组哨兵

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    // 无参构造专用哨兵,靠它区分"第一次 add 要不要直接扩到 10"

    transient Object[] elementData; // non-private to simplify nested class access
                                    // transient:不参与默认序列化,writeObject 只写前 size 个元素,尾部空位不落盘

    private int size;    // 实际元素个数,!= elementData.length(后者是容量)

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;    // 显式要 0,不会自动膨胀到 10
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    // 懒分配:大量 list 建了从不用,白占 10 格太亏
    }

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;    // 精确是 ArrayList 才敢直接接管,因为它的 toArray 保证返回全新 Object[]
            } else {
                // 防 JDK-6260652 一类坑:子类/别的集合的 toArray 可能返回 String[] 等更窄类型,
                // 直接接管后 elementData[i] = 任意对象 会抛 ArrayStoreException,必须复制成 Object[]
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

    // ...

    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 新容量 = 旧容量 + max(最小增量, 旧容量>>1),即通常 1.5 倍(不是 2 倍);
            // newLength 内部处理 int 溢出,先顶到 SOFT_MAX(Integer.MAX_VALUE-8),实在不够才逼近 Integer.MAX_VALUE
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);    // O(n) 整体拷贝,扩容的全部代价在这
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];    // 无参构造的第一次分配:max(10, 实际需要)
        }
    }

    private Object[] grow() {
        return grow(size + 1);    // 至少要能再放一个
    }

    // ...

    public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {    // 还挂在默认哨兵上且要的不超过 10:反正第一次 add 会给 10,现在膨胀是浪费
            modCount++;    // 坑:扩容也算结构性修改,迭代期间调 ensureCapacity 同样引爆 fail-fast
            grow(minCapacity);
        }
    }

    // ...

    // 源码注释原话:从 add(E) 拆出这个私有方法,是为了让 add(E) 字节码 < 35 字节(-XX:MaxInlineSize 默认值),
    // C1 编译的循环里调 add 才能被内联。性能敏感到按字节码大小抠方法边界
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)    // 满了才扩,等号成立即 size 撞上容量
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    public boolean add(E e) {
        modCount++;    // 结构性修改计数,fail-fast 的唯一依据
        add(e, elementData, size);
        return true;
    }
}
// 基于本地 JDK 源码 (java.base, JDK 21+ 代码线), java.util.ArrayList —— 按位读写与删除
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // ...

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];    // 泛型擦除后数组是 Object[],读出来强转,@SuppressWarnings 收敛在此一处
    }

    public E get(int index) {
        Objects.checkIndex(index, size);    // 按 size 检查而不是数组长度,容量内但 >= size 的位置照样越界
        return elementData(index);          // O(1),RandomAccess 的底气
    }

    public E set(int index, E element) {
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
        elementData[index] = element;    // 全程不碰 modCount:set 不是结构性修改,迭代期间 set 不会触发 fail-fast
        return oldValue;
    }

    // 头部/中间插入触发 arraycopy 搬动后续所有元素,越靠前越慢,这就是"中间增删 O(n)"的来源
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);         // [index, size) 整体后移一位腾位置
        elementData[index] = element;
        size = s + 1;
    }

    public E remove(int index) {
        Objects.checkIndex(index, size);
        final Object[] es = elementData;

        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        fastRemove(es, index);

        return oldValue;
    }

    // 按值删除:只删第一个匹配。注意 list.remove(1) 走上面按下标的重载,Integer 列表想按值删要 remove(Integer.valueOf(1))
    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            if (o == null) {                 // null 单独一条路,因为 null.equals 会 NPE,所以 ArrayList 允许存 null
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))     // 方向是 参数.equals(元素),元素侧的 equals 不会被调用
                        break found;
            }
            return false;                    // 没找到,modCount 不动
        }
        fastRemove(es, i);
        return true;
    }

    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);    // [i+1, size) 前移覆盖被删元素
        es[size = newSize] = null;    // 末位置 null 帮 GC,否则残留引用让对象无法回收(逻辑删除但物理还挂着)
    }
}
// 基于本地 JDK 源码 (java.base, JDK 21+ 代码线), java.util.ArrayList —— fail-fast 迭代器
public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;    // 创建迭代器时拍下的"修改快照"

    // ...

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();    // 每次 next 先验快照
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();    // 兜底:并发下数组引用可能已被换成更短的(如 trimToSize),快照来不及发现
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();    // 还没 next 过、或已删过一次,没有"上一个元素"可删
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;                     // 后续元素前移了一位,游标退回去才不漏元素
            lastRet = -1;
            expectedModCount = modCount;          // 关键:同步快照。这就是"边遍历边删必须用 it.remove()"的原因
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    // ...

    final void checkForComodification() {
        if (modCount != expectedModCount)    // 快照对不上 = 迭代期间有别的途径做了结构性修改
            throw new ConcurrentModificationException();
    }
}

原理串讲

new ArrayList<>() 到第一次 add("a") 走一遍。构造器什么都不分配,只把 elementData 挂到哨兵 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 上。
add(E)modCount++,再把 elementDatasize 作为参数传给私有的 add(E, Object[], int);此时 s <mark> elementData.length(0 0)成立,进 grow()grow(size + 1)
grow(1)oldCapacity == 0 且数组正是默认哨兵,走 else 分支直接 new Object[Math.max(10, 1)],一步到位分配 10 格——这就是”第一次 add 才扩到 10”的落点。
之后第 11 次 add 再次撞满,这回走 if 分支:ArraysSupport.newLength(10, 1, 5)max(最小增量 1, 期望增量 5) 得新容量 15,Arrays.copyOf 做一次 O(n) 拷贝换新数组。

为什么要两个内容完全相同的空数组哨兵?因为语义不同:EMPTY_ELEMENTDATA 是用户显式要的 0 容量,后续扩容从实际需求按 1.5 倍走;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是”我还欠你一个默认容量 10”的记账凭证,growelementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个引用比较(不是 equals)识别出欠账并一次性给足 10。
两个哨兵都是 static 共享的,千万个空 list 只占两个对象。

为什么 add(E) 要拆出一个私有重载?源码注释写得很直白:让 add(E) 的字节码保持在 35 字节以下(-XX:MaxInlineSize 的默认值),这样 C1 编译的热循环里 list.add(x) 能被内联。JDK 核心类的方法边界是按 JIT 内联阈值抠出来的,不是按可读性。

删除侧的链路:remove(int) 做完 Objects.checkIndex 后取出旧值,交给 fastRemove——后者不做边界检查也不返回值,所以按值删除的 remove(Object) 找到下标后可以直接复用它,省一次重复检查。
fastRemove[i+1, size) 前移一位,再把腾出的末位写 null;不写 null 的话,元素虽然逻辑上已删除,数组尾部仍持有强引用,GC 收不走。

代码块JAVA · 2 行收起展开
fail-fast 的完整因果:每次结构性修改(add/remove/clear/grow 相关路径)都 `modCount++`;迭代器创建时拍快照 `expectedModCount = modCount`,每次 `next()` 都比对。
`for (e : list) { list.remove(e); }` 抛 ConcurrentModificationException,就是因为 `list.remove` 改了 `modCount` 却没人更新迭代器的快照,下一次 `next()` 的 `checkForComodification()` 当场引爆;

it.remove() 删完会执行 expectedModCount = modCount 重新对齐,同时把 cursor 退回 lastRet(后面的元素整体前移了一位,不退会跳过一个元素)。
注意 modCount 不是 volatile,这套机制只保证”大概率发现 bug”,不保证并发下一定抛,更不能当同步手段用。

设计取舍

  • 1.5 倍而不是 2 倍扩容:增长更平缓、浪费的尾部空间更少;理论上 1.5 倍时之前释放的旧数组空间之和有机会被后续扩容复用,2 倍永远不能。
  • 扩容只发生在”撞满”瞬间,能预估规模就用 new ArrayList<>(n)ensureCapacity,把 N 次 Arrays.copyOf 压成 0 次。
  • fail-fast 是排错手段不是线程安全:并发场景要么 Collections.synchronizedList + 手动锁迭代,要么 CopyOnWriteArrayList,别指望 CME 一定出现。
  • 「结构性修改」特指改变 size 或换底层数组(add/remove/clear/ensureCapacity 等);set/get 不碰 modCount,迭代期间 set 不炸。
  • 边遍历边删:单线程用 it.remove()removeIf;for (e : list) 里调 list.remove 必炸。

延伸阅读