【Java集合】ArrayList从入门到精通:原理、源码、扩容机制一文讲透
【Java集合】ArrayList从入门到精通:原理、源码、扩容机制一文讲透
前言
ArrayList是Java开发中使用频率最高的集合类之一,几乎每个Java程序员每天都会与它打交道。然而,你真的了解ArrayList吗?你知道它是如何扩容的吗?你清楚remove()方法和clear()方法的区别吗?本文将从底层原理到源码实现,全方位深度解析ArrayList,让你从「会用」进化到「精通」。
一、ArrayList的核心特性
1.1 ArrayList的本质
ArrayList的底层数据结构是一个动态数组(Dynamic Array),这意味着它在内存中是连续存储的。与数组相比,ArrayList的优势在于可以自动扩容,不需要开发者在创建时指定固定大小。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组实例(用于初始化)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 存储元素的数组
transient Object[] elementData;
// 集合实际大小
private int size;
}
从源码可以看出,ArrayList内部维护了一个Object[] elementData数组来存储元素,这就是ArrayList能够随机访问的基础。
1.2 ArrayList与数组的区别
| 特性 | Array | ArrayList |
|---|---|---|
| 大小固定 | 创建时确定,不可变 | 动态扩容,自动调整 |
| 类型安全 | 编译期确定 | 泛型支持,类型安全 |
| 性能 | 插入删除O(n),随机访问O(1) | 同数组 |
| 灵活性 | 低 | 高 |
| 存储类型 | 基本类型和对象 | 仅对象类型 |
需要特别注意的是,ArrayList不能存储基本数据类型(如int、char),必须使用其包装类(如Integer、Character)。
1.3 ArrayList的继承体系
┌─────────────────┐
│ Iterable │
└────────┬────────┘
│
┌────────▼────────┐
│ Collection<E> │
└────────┬────────┘
│
┌────────▼────────┘
│ List<E> │
└────────┬────────┘
│
┌────────▼────────┐
│AbstractList<E> │
└────────┬────────┘
│
┌────────▼────────┐
│ ArrayList<E> │
└─────────────────┘
ArrayList实现了RandomAccess接口,这是一个标记接口,表示ArrayList支持快速随机访问。这意味着通过索引访问元素的时间复杂度是O(1),这是数组的特性决定的。
二、ArrayList常用方法深度解析
2.1 add()方法:添加元素的完整流程
ArrayList提供了多个add方法,我们逐个分析:
单元素添加
public boolean add(E e) {
// 确保容量足够,如果不够则扩容
ensureCapacityInternal(size + 1);
// 将元素放入数组,并移动指针
elementData[size++] = e;
return true;
}
指定位置插入
public void add(int index, E element) {
// 检查索引合法性
rangeCheckForAdd(index);
// 确保容量足够
ensureCapacityInternal(size + 1);
// 将index位置及之后的元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 插入新元素
elementData[index] = element;
size++;
}
从源码可以看出,add(int index, E element)方法由于需要移动元素,其时间复杂度是O(n)。而add(E e)方法直接追加到末尾,如果没有触发扩容,时间复杂度是O(1)。
2.2 get()方法:随机访问的实现
public E get(int index) {
// 检查索引合法性
rangeCheck(index);
// 直接通过数组下标访问
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
这就是ArrayList支持快速随机访问的秘密——通过数组下标直接访问元素,时间复杂度恒为O(1)。
2.3 remove()方法:删除元素的细节
按索引删除
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 计算需要移动的元素数量
int numMoved = size - index - 1;
if (numMoved > 0) {
// 将index之后的元素向前移动一位
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
// 释放最后一个元素的引用
elementData[--size] = null;
return oldValue;
}
按元素删除
public boolean remove(Object o) {
if (o == null) {
// 处理null元素
for (int index = 0; index < size; index++) {
if (elementData[index] == null) {
fastRemove(index);
return true;
}
}
} else {
// 遍历查找元素
for (int index = 0; index < size; index++) {
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
}
2.4 clear()方法:清空集合的差异
public void clear() {
modCount++;
// 将所有元素置为null,帮助GC
for (int i = 0; i < size; i++) {
elementData[i] = null;
}
size = 0;
}
注意:clear()方法只是将数组中的元素置为null,并不释放底层数组的内存空间。如果希望释放内存,可以在clear()后将ArrayList置为null。
2.5 trimToSize()方法:内存优化
public void trimToSize() {
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}
这个方法非常实用!如果ArrayList中存储的元素远少于数组容量,调用trimToSize()可以释放多余的内存空间,使数组大小与实际元素数量一致。
三、ArrayList扩容机制深度剖析
3.1 扩容的核心方法
ArrayList的扩容机制是其最核心、最精彩的设计之一。让我们详细分析:
private void ensureCapacityInternal(int minCapacity) {
// 判断是否是第一次添加元素
if (elementData == EMPTY_ELEMENTDATA) {
// 设置最小容量为默认值10和所需容量的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保容量足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需容量大于当前数组长度,则需要扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
private void grow(int minCapacity) {
// 记录原数组长度
int oldCapacity = elementData.length;
// 计算新容量:原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量小于所需容量,则使用所需容量
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 如果新容量超过最大数组大小,做进一步处理
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 核心:创建新数组并复制元素
elementData = Arrays.copyOf(elementData, newCapacity);
}
3.2 扩容流程图解
初始状态:ArrayList容量为10,已存储5个元素
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │null│null│null│null│null│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↑
size=5
现在要添加第6个元素,需要扩容:
第一步:计算新容量
oldCapacity = 10
newCapacity = 10 + (10 >> 1) = 10 + 5 = 15
第二步:创建新数组
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │null│null│null│null│null│null│null│null│null│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↑
size=6, capacity=15
3.3 扩容策略的巧妙设计
ArrayList选择扩容50%(1.5倍)而非翻倍,这是一个经过深思熟虑的设计决策:
1. 空间与时间的平衡
- 扩容100%(翻倍):空间浪费严重,但扩容频率低
- 扩容50%:空间利用率更高,但扩容更频繁
- ArrayList选择50%,是一个平衡点
2. 减少内存碎片
适度的扩容倍数有助于减少内存碎片,使内存分配更加高效。
3. 经验与实践验证
经过大量实践验证,50%的扩容倍数在大多数场景下表现最优。
3.4 初始容量的影响
我们来看一个性能对比:
场景1:不指定初始容量
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("item" + i);
}
执行过程:空数组 → 容量10 → 容量15 → 容量22 → … → 容量100+
共经历多次扩容,效率较低。
场景2:指定合适的初始容量
ArrayList<String> list = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
list.add("item" + i);
}
执行过程:直接创建容量为100的数组,无扩容。
性能建议:如果能够预估集合大小,强烈建议在创建ArrayList时指定初始容量,避免频繁扩容带来的性能开销。
四、ArrayList的Fail-Fast机制
4.1 modCount的作用
ArrayList中有一个modCount字段,它记录了集合被修改的次数:
protected transient int modCount = 0;
每次调用add()、remove()、clear()等修改集合结构的方法时,modCount都会增加。
4.2 迭代器的Fail-Fast检测
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的索引
int lastRet = -1; // 上一个返回元素的索引
int expectedModCount = modCount; // 记录创建迭代器时的modCount
public E next() {
checkForComodification(); // 检查modCount是否变化
// ... 获取下一个元素
}
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
4.3 Fail-Fast的意义
Fail-Fast机制的目的是在检测到并发修改时立即抛出异常,避免出现不可预期的行为:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// 在迭代过程中修改集合
Iterator<String> iterator = list.iterator();
list.add("d"); // 修改modCount
iterator.next(); // 抛出 ConcurrentModificationException
这种机制虽然不能完全解决并发修改问题,但能够在开发阶段帮助我们发现潜在的问题。
4.4 如何安全地遍历和删除
错误写法
for (String item : list) {
if ("b".equals(item)) {
list.remove(item); // 可能抛出ConcurrentModificationException
}
}
正确写法一:使用Iterator
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("b".equals(item)) {
iterator.remove(); // 使用迭代器的remove方法
}
}
正确写法二:使用removeIf(Java 8+)
list.removeIf("b"::equals);
正确写法三:倒序遍历
for (int i = list.size() - 1; i >= 0; i--) {
if ("b".equals(list.get(i))) {
list.remove(i);
}
}
五、ArrayList与其他集合的对比
5.1 ArrayList vs LinkedList
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1),支持 | O(n),不支持 |
| 头部插入/删除 | O(n) | O(1) |
| 尾部插入/删除 | O(1)(均摊) | O(1) |
| 中间插入/删除 | O(n) | O(n),但不需要移动元素 |
| 内存占用 | 连续内存,可能产生碎片 | 每个节点需要额外空间存储指针 |
| 缓存友好 | 高 | 低 |
选择建议:
- 需要频繁随机访问 → ArrayList
- 需要频繁在头部/尾部操作 → LinkedList
- 不知道选什么 → 先选ArrayList
5.2 ArrayList vs Vector
| 特性 | ArrayList | Vector |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全 |
| 性能 | 高 | 低( synchronized) |
| 扩容策略 | 1.5倍 | 2倍(可配置) |
| 迭代器 | 快速失败 | 安全但效率低 |
选择建议:如果需要线程安全的List,使用Collections.synchronizedList()包装ArrayList,或者直接使用CopyOnWriteArrayList,性能优于Vector。
5.3 ArrayList vs HashSet
| 特性 | ArrayList | HashSet |
|---|---|---|
| 数据结构 | 动态数组 | 哈希表 |
| 元素顺序 | 保持插入顺序 | 不保证顺序 |
| 重复元素 | 允许 | 不允许 |
| 查找效率 | O(n) | O(1) |
| 随机访问 | 支持 | 不支持 |
六、性能优化最佳实践
6.1 预分配内存
// 不推荐:每次添加都可能触发扩容
List<String> list = new ArrayList<>();
// 推荐:已知大概数量,预分配内存
List<String> list = new ArrayList<>(1000);
6.2 使用ArrayList.forEach替代for循环
// 传统写法
for (String item : list) {
System.out.println(item);
}
// Java 8+写法,更简洁
list.forEach(System.out::println);
6.3 使用trimToSize释放内存
// 在大量数据处理完成后,释放多余内存
list.addAll(largeDataSet);
processData(list);
((ArrayList<String>) list).trimToSize();
6.4 批量操作的性能优化
// 低效:每次add都触发可能的扩容
List<String> list = new ArrayList<>();
for (String item : largeCollection) {
list.add(item);
}
// 高效:使用addAll一次性添加
List<String> list = new ArrayList<>(largeCollection.size());
list.addAll(largeCollection);
6.5 避免在循环中调用size()
// 低效:每次循环都调用size()方法
for (int i = 0; i < list.size(); i++) {
// 操作
}
// 高效:缓存size
int size = list.size();
for (int i = 0; i < size; i++) {
// 操作
}
七、常见面试题解答
7.1 ArrayList的默认初始容量是多少?
ArrayList的默认初始容量是10,但只有在第一次添加元素时才会真正分配10个空间的数组。如果使用new ArrayList<>()创建空集合,实际初始容量是0。
7.2 ArrayList的扩容机制是什么?
当容量不足时,ArrayList会创建一个原容量1.5倍的新数组,然后使用Arrays.copyOf()将原数组元素复制到新数组中。这是均摊O(1)复杂度的关键。
7.3 ArrayList是线程安全的吗?
ArrayList不是线程安全的。在多线程环境下,可能出现ConcurrentModificationException或数据丢失问题。如需线程安全,推荐使用CopyOnWriteArrayList或使用Collections.synchronizedList()包装。
7.4 ArrayList的remove和clear有什么区别?
remove(int index):删除指定位置的元素,返回被删除的元素remove(Object o):删除第一个匹配的元素,返回是否删除成功clear():删除所有元素,将size置为0,但保留原数组容量
7.5 ArrayList如何实现随机访问?
ArrayList底层是数组实现,通过索引直接访问数组元素,时间复杂度O(1)。这也是它实现RandomAccess接口的原因。
八、总结
ArrayList作为Java中最常用的集合类,其设计体现了Java语言的设计哲学:
- 简单:基于数组,操作直观
- 高效:随机访问O(1),尾部插入均摊O(1)
- 灵活:自动扩容,动态调整
- 安全:Fail-Fast机制,及时发现并发问题
理解ArrayList的底层原理,不仅能帮助我们写出更高性能的代码,还能为学习其他集合类打下坚实的基础。
如果本文对你有帮助,欢迎点赞、收藏、转发!
关注我,获取更多Java核心技术文章。
参考阅读:
- JDK 8源码:java.util.ArrayList
- 《Effective Java》第三版 - 第50条:优先考虑泛型
- Java官方文档:ArrayList API文档
更多推荐


所有评论(0)