Java基础系列-ArrayList源码解析
文章目录
- **核心数据结构**
- **动态扩容机制**
- **添加元素流程**
- 整体流程(以 add(E e) 为例)
- **场景1:第一次添加元素(空数组扩容)**
- **场景2:末尾插入且容量充足**
- **场景3:触发扩容(容量不足)**
- **扩容核心方法 `grow()`**
- **安全检查方法 `hugeCapacity(minCapacity)`**
- 拓展1:`MAX_ARRAY_SIZE` **的存在意义**
- **拓展2:**`hugeCapacity` **处理溢出**
- **拓展3:为什么允许返回** `Integer.MAX_VALUE`
- **场景4:中间插入(需移动元素)- `add(int index, E e)`**
- **核心方法 `add(int index, E e)`**
- **删除元素流程(以 remove(int index) 为例)**
- **ModCount 作用**
- **线程安全性**
- **性能优化技巧**
- **与 LinkedList 的对比**
Java 的 ArrayList
是集合框架中最核心的动态数组实现,也是高频使用的容器之一。
核心数据结构
基于数组实现,维护 elementData
数组存储元素:
// JDK 1.8
transient Object[] elementData; // 实际存储元素的数组
private int size; // 当前元素数量
transient
修饰的elementData
不会被默认序列化(通过自定义序列化逻辑优化存储)
动态扩容机制
当添加元素时发现容量不足,触发 grow(int minCapacity)
扩容:
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为旧容量的1.5倍if (newCapacity - minCapacity < 0)newCapacity = minCapacity; // 最小需要满足 minCapacityelementData = Arrays.copyOf(elementData, newCapacity);
}
核心逻辑:
-
扩容倍率:新容量 = 旧容量 * 1.5(位运算
oldCapacity >> 1
代替除法优化性能) -
数组拷贝:
Arrays.copyOf()
底层使用System.arraycopy()
,为本地方法(效率高)
添加元素流程
整体流程(以 add(E e) 为例)
public boolean add(E e) {ensureCapacityInternal(size + 1); // 确保容量足够elementData[size++] = e; // 尾部插入元素return true;
}
-
容量检查:若当前数组已满,触发扩容后再插入
-
尾部插入:时间复杂度 O(1)
场景1:第一次添加元素(空数组扩容)
-
触发条件:默认构造的 ArrayList(初始
elementData
为空数组)首次调用add()
。 -
流程:
-
minCapacity = size + 1 = 1
-
判断当前容量
elementData.length = 0
,需要扩容。
-
计算新容量
newCapacity = max(10, 1)
➔newCapacity = 10
. -
新数组
elementData = new Object[10]
,将元素插入首位。
-
场景2:末尾插入且容量充足
-
流程:
-
ensureCapacityInternal(size + 1)
➔ 无需扩容。 -
elementData[size++] = e
➔ 直接插入末尾,无需移动元素,O(1) 时间复杂度。
-
场景3:触发扩容(容量不足)
-
例:数组已满(
size == elementData.length
)时添加新元素。 -
扩容步骤:
-
oldCapacity = 10
. -
计算增长量
oldCapacity >> 1 = 5
➔newCapacity = 15
.
-
Arrays.copyOf(elementData, 15)
➔ 快速本地方法拷贝数组。 -
扩容代价:数组拷贝 O(n),需在插入时尽量避免频繁扩容。
-
扩容核心方法 grow()
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 计算1.5倍扩容if (newCapacity - minCapacity < 0) {newCapacity = minCapacity; // 手动调整至至少满足 minCapacity}if (newCapacity - MAX_ARRAY_SIZE > 0) {newCapacity = hugeCapacity(minCapacity); // 安全检查(最大容量限制)}elementData = Arrays.copyOf(elementData, newCapacity);
}
安全检查方法 hugeCapacity(minCapacity)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // 溢出判断:如果 minCapacity 已经超过 Integer.MAX_VALUE,会变成负数throw new OutOfMemoryError(); // 比较需求容量和最大数组容量约束(Integer.MAX_VALUE - 8)return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
hugeCapacity
的决策逻辑分为:
-
情况 1:
minCapacity
正常(非负且 <=MAX_ARRAY_SIZE
)→ 返回
MAX_ARRAY_SIZE
(即Integer.MAX_VALUE - 8
)。 -
情况 2:
minCapacity > MAX_ARRAY_SIZE
→ 直接返回
Integer.MAX_VALUE
(允许尝试分配更大的容量,但可能导致 OOM)。 -
异常处理:
minCapacity < 0
(溢出导致)→ 抛出
OutOfMemoryError
(申请容量已超过Integer.MAX_VALUE
,无法满足)。
拓展1:MAX_ARRAY_SIZE
的存在意义
Java 规范允许 JVM 实现为数组对象分配内存时保留一部分头部信息(如对象头、数组长度等),可能导致某些情况下实际数组最大可用容量小于 Integer.MAX_VALUE
。约定 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
是为了避免直接分配 Integer.MAX_VALUE
导致内存不足,属于一种保护性设计。
拓展2:hugeCapacity
处理溢出
当 minCapacity
的值因为连续扩容超过 Integer.MAX_VALUE
时,会变成一个负数(整数溢出)。此时 minCapacity < 0
的检查会立即抛出 OutOfMemoryError
,避免试图创建非法数组。
拓展3:为什么允许返回 Integer.MAX_VALUE
在 JVM 实现支持的情况下,部分版本的 JVM 可能允许数组实际容量接近 Integer.MAX_VALUE
。hugeCapacity
在此刻做了一个折中决策:
-
优先尝试返回
Integer.MAX_VALUE
(信任 JVM 能处理)。 -
若 JVM 实际无法分配,则会在
Arrays.copyOf()
阶段抛出OutOfMemoryError
。
场景4:中间插入(需移动元素)- add(int index, E e)
list.add(2, "hello"); // 在索引2处插入元素
-
流程:
-
检查索引合法性(
index >= 0 && index <= size
)。 -
检查容量 ➔ 不够则扩容。
-
计算需要移动的元素数量:
numMoved = size - index = 3
. -
调用
System.arraycopy(elementData, 2, elementData, 3, numMoved)
,将原数据从索引2开始的元素后移1位。
-
elementData[2] = "hello"
. -
时间复杂度:平均 O(n).
-
核心方法 add(int index, E e)
public void add(int index, E element) {rangeCheckForAdd(index); // 索引范围检查ensureCapacityInternal(size + 1); // 移动元素:需向后移的元素数量 = size - indexSystem.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;
}
删除元素流程(以 remove(int index) 为例)
public E remove(int index) {rangeCheck(index); // 索引越界检查modCount++; // 结构性修改次数(用于迭代器快速失败)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; // 清空原末尾元素,帮助GCreturn oldValue;
}
核心点:
-
移动代价:平均时间复杂度 O(n),末尾删除为 O(1)
-
GC处理:手动赋
null
避免内存泄漏
ModCount 作用
迭代器通过 modCount
追踪结构性修改:
private class Itr implements Iterator<E> {int expectedModCount = modCount; // 记录创建迭代器时的修改次数public E next() {checkForComodification(); // 检查是否发生结构性修改// ... 其他逻辑}final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
}
-
结构性修改:任何导致
size
变化或元素位置变化的操作(增、删、排序等) -
多线程问题:未同步时快速失败机制能检测部分并发问题
线程安全性
-
非线程安全:
ArrayList
的设计不保证多线程环境下的安全 -
替代方案:
-
Collections.synchronizedList(new ArrayList<>())
同步包装类 -
CopyOnWriteArrayList
写时复制容器(适合读多写少场景)
-
性能优化技巧
-
初始化时指定容量:避免频繁扩容
ArrayList<String> list = new ArrayList<>(1000); // 直接指定初始容量
-
批量操作优先:避免循环内多次扩容
list.addAll(Arrays.asList("A", "B", "C")); // 批量添加减少扩容次数
- 谨慎使用 contains/remove(Object):时间复杂度 O(n),高频操作可改用
HashSet
与 LinkedList 的对比
操作 | ArrayList | LinkedList |
---|---|---|
随机访问 | O(1)(通过索引) | O(n)(需要遍历链表) |
头部插入/删除 | O(n)(需移动元素) | O(1)(只需调整指针) |
内存占用 | 紧凑(无需指针) | 每个元素需额外指针空间 |
适用场景 | 读多写少、随机访问高频 | 频繁增删、顺序访问 |