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++,再把 elementData 和 size 作为参数传给私有的 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”的记账凭证,grow 靠 elementData != 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 收不走。
代码块收起展开
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必炸。