1. 核心概念与底层实现
1.1 ArrayList 的本质
ArrayList
是基于 动态数组 的 List
实现类,其底层数据结构是 Object[] elementData
。它通过动态扩容机制(自动扩展数组长度)实现灵活的存储需求,但牺牲了线程安全性。
- 定义:
ArrayList
是Java集合框架中的一个动态数组实现类,继承自AbstractList
,实现了List
接口。它允许存储重复元素和null值,并支持通过索引快速访问元素。 - 核心特性:
- 动态数组:容量可自动扩展,无需手动管理。
- 线程不安全:多线程环境下需手动同步或使用
Vector
/CopyOnWriteArrayList
。 - 随机访问高效:通过索引访问元素的时间复杂度为O(1)。
- 增删操作较慢:中间位置的插入/删除需要移动元素,时间复杂度为O(n)。
1.1.1 底层数据结构
ArrayList
的底层基于对象数组(Object[] elementData
)实现:
- 初始容量:默认为10(通过
DEFAULT_CAPACITY
定义)。 - 动态扩容:当数组空间不足时,自动扩容为原容量的1.5倍(
oldCapacity + (oldCapacity >> 1)
)。 - 关键成员变量:
// 底层数组,存储所有元素 transient Object[] elementData; // 当前元素个数 private int size;
JDK 1.7 vs JDK 1.8的初始化差异
特性 | JDK 1.7 | JDK 1.8+ |
---|---|---|
无参构造的elementData.length | 10 | 0(DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) |
首次添加元素时的扩容 | 不触发(已预分配) | 触发扩容到10 |
内存占用 | 立即分配10个元素的内存空间 | 延迟分配,节省初始内存 |
1.1.2 动态扩容机制
扩容触发条件:
当调用add()
方法时,若当前数组容量(elementData.length
)小于size + 1
,则触发扩容。
扩容规则:
- 新容量计算:
newCapacity = oldCapacity + (oldCapacity >> 1)
(即原容量的1.5倍)。 - 特殊情况处理:
- 若计算后的容量仍小于所需最小容量(
minCapacity
),则直接使用minCapacity
。 - 若扩容过程中出现整数溢出(极端大容量),抛出
OutOfMemoryError
。
- 若计算后的容量仍小于所需最小容量(
1.2 关键成员变量
// 核心成员变量
transient Object[] elementData; // 底层数组,存储元素
private int size; // 当前元素个数
// 静态常量
private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; // 空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; // 无参构造时使用
private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 最大数组长度
2. 初始化机制:从0到10的蜕变
2.1 无参构造的陷阱
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化为空数组(长度0)
}
- 关键点:
elementData.length
初始为0,但DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一个静态空数组(长度0)。- 首次添加元素时,会触发 强制扩容到10,而非直接使用
elementData
的原始长度。
2.2 首次扩容的触发过程
当调用 add()
方法时:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 此处触发扩容
elementData[size++] = e;
return true;
}
2.2.1 ensureCapacityInternal
的核心逻辑
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 强制设置为10
}
ensureExplicitCapacity(minCapacity);
}
- 关键步骤:
- 检查
elementData
是否为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
(即无参构造的空数组)。 - 若是,则将
minCapacity
设为max(10, minCapacity)
,确保首次扩容至少到10。 - 调用
ensureExplicitCapacity
继续检查。
- 检查
2.2.2 ensureExplicitCapacity
的逻辑
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果当前容量不足
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
- 触发扩容条件:
minCapacity > elementData.length
。
2.2.3 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); // 复制数据到新数组
}
- 首次扩容时:
oldCapacity = 0
→newCapacity = 0 + 0 = 0
,但经过ensureCapacityInternal
的修正后,minCapacity = 10
。- 最终
newCapacity = 10
,触发Arrays.copyOf
创建新数组。
3. 容量演变的详细过程
初始容量的定义
- 默认初始容量:
ArrayList
的默认初始容量是10,但这仅在首次添加元素时生效。 - 底层数组的初始状态:
- JDK 1.7:无参构造时直接创建长度为10的数组(预分配策略)。
- JDK 1.8+:无参构造时底层数组初始化为空数组(长度为0),采用懒汉式初始化,直到首次调用
add()
方法时才会分配容量为10的数组。
关键成员变量
// 源码片段(JDK 1.8+)
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量为10
private static final int DEFAULT_CAPACITY = 10;
// 空数组,用于区分不同状态的空ArrayList
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 无参构造时使用
// 底层数组,存储元素
transient Object[] elementData;
// 当前元素个数
private int size;
}
无参构造方法的初始化
// 无参构造方法(JDK 1.8+)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化为空数组
}
elementData
的初始状态:elementData
被初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,其长度为0。- 此时,
ArrayList
的容量(elementData.length
)为0,但默认容量(DEFAULT_CAPACITY
)是10。
首次添加元素时的初始化
当调用add()
方法添加第一个元素时,会触发以下流程:
// add(E e)方法(JDK 1.8+)
public boolean add(E e) {
// 确保容量足够
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果当前elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即无参构造的情况)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 将minCapacity设为max(DEFAULT_CAPACITY, minCapacity)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果当前容量不足
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 关键步骤:
- 当
elementData
是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
时(即无参构造的空列表),minCapacity
被强制设为Math.max(DEFAULT_CAPACITY, minCapacity)
。 - 此时,
minCapacity
为10(假设首次添加一个元素,minCapacity = 1
,但会被替换为10)。 - 调用
grow(minCapacity)
扩容到10。
- 当
初始容量的动态变化
- JDK 1.8+的初始容量流程:
- 无参构造时:
elementData.length = 0
,size = 0
。 - 首次添加元素时:扩容到10,
elementData.length = 10
,size = 1
。 - 后续添加元素时:当
size
达到10时,触发下一次扩容(10 → 15)。
- 无参构造时:
3.1 不同场景的容量变化
3.1.1 无参构造的初始状态
ArrayList<String> list = new ArrayList<>();
System.out.println(list.size()); // 0
System.out.println(list.elementData.length); // 0(JDK 1.8+)
3.1.2 首次添加元素
list.add("Hello");
// 此时:
System.out.println(list.size()); // 1
System.out.println(list.elementData.length); // 10(扩容到10)
3.1.3 添加第11个元素
for (int i = 1; i < 10; i++) {
list.add("World");
}
list.add("World"); // 第11个元素
// 此时:
System.out.println(list.size()); // 11
System.out.println(list.elementData.length); // 15(10 → 15)
3.1.4 添加第16个元素
for (int i = 0; i < 5; i++) {
list.add("Java");
}
// 此时:
System.out.println(list.elementData.length); // 22(15 → 22)
4. 扩容的数学模型与性能分析
4.1 扩容的数学公式
- 扩容公式:
newCapacity = oldCapacity + (oldCapacity >> 1)
等价于newCapacity = oldCapacity * 1.5
(向下取整)。
4.2 扩容的渐进特性
当前容量 | 新容量计算 | 新容量实际值 |
---|---|---|
0 | 0 + 0 | 10(强制修正) |
10 | 10 + 5 | 15 |
15 | 15 + 7 | 22 |
22 | 22 + 11 | 33 |
4.3 扩容的性能代价
- 时间复杂度:
每次扩容需复制所有元素,时间复杂度为 O(n),但通过 指数增长策略,总扩容时间复杂度为 O(n)(摊还分析)。 - 空间复杂度:
最终容量可能超过实际需求,但通过trimToSize()
(JDK 11+)可回收冗余空间。
5. 核心方法的源码解析
5.1 尾部添加 add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e; // 直接写入数组末尾
return true;
}
- 时间复杂度:O(1)(不考虑扩容开销)。
5.2 中间插入 add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index); // 索引检查
ensureCapacityInternal(size + 1);
// 将[index, size) 的元素后移一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
- 时间复杂度:O(n)(需移动元素)。
5.3 删除元素 remove(int index)
public E remove(int index) {
rangeCheck(index); // 索引检查
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null; // 释放引用
return oldValue;
}
- 时间复杂度:O(n)(需移动元素)。
6. 特殊场景与常见问题
6.1 初始容量为0的误解
- 常见误区:认为
ArrayList
的初始容量是0,但实际:- JDK 1.8+:无参构造时
elementData.length = 0
,但首次添加元素时强制扩容到10。 size
的初始值始终为0,直到元素被添加。
- JDK 1.8+:无参构造时
6.2 扩容溢出的处理
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) { // 无法处理负数
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
- 作用:当扩容请求超过
MAX_ARRAY_SIZE
(2^31-9)时,使用Integer.MAX_VALUE
。
6.3 并发修改异常(ConcurrentModificationException)
- 触发条件:在迭代过程中修改集合(非通过迭代器)。
- 解决方案:
List<String> list = new ArrayList<>(); // 错误示例: for (String s : list) { if (s.equals("remove")) { list.remove(s); // 抛出异常 } } // 正确示例: Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("remove")) { it.remove(); // 安全删除 } }
7. 性能优化与最佳实践
7.1 预分配容量
// 优化示例:已知数据量时预分配容量
ArrayList<String> list = new ArrayList<>(1000000); // 初始容量100万
for (int i = 0; i < 1000000; i++) {
list.add("Data" + i);
}
7.2 避免频繁扩容
- 场景:添加大量元素时,预分配容量可减少扩容次数。
- 数学证明:
- 假设初始容量为
C
,每次扩容增长1.5倍,添加N
个元素时,扩容次数为:log_1.5(N / C)
- 预分配
C = N
可完全避免扩容。
- 假设初始容量为
7.3 使用 trimToSize()
// JDK 11+:回收冗余容量
list.trimToSize(); // 将数组长度调整为当前size
8. 与 LinkedList 的深度对比
8.1 底层结构对比
特性 | ArrayList | LinkedList |
---|---|---|
存储结构 | 连续内存数组 | 双向链表(每个节点存储前后指针) |
访问速度 | 快(O(1)) | 慢(O(n)需遍历) |
插入/删除速度 | 慢(中间操作需移动元素,O(n)) | 快(修改指针,O(1)) |
内存占用 | 低(连续内存) | 高(每个节点存储额外指针) |
8.2 适用场景
- ArrayList:
- 频繁查询、少量增删。
- 数据量较大但访问模式固定。
- LinkedList:
- 频繁增删、少量查询。
- 需要双向遍历或链表特性(如队列、栈)。
9. 源码级优化技巧
9.1 避免 toArray()
的性能陷阱
// 错误示例:频繁调用toArray()导致额外开销
for (Object obj : list.toArray()) {
// ...
}
// 优化示例:直接使用elementData(需谨慎)
Object[] arr = list.toArray();
for (Object obj : arr) {
// ...
}
9.2 使用 subList()
的注意事项
// 避免直接修改子列表的引用
List<String> sublist = list.subList(0, 10);
sublist.clear(); // 会修改原列表
10. 总结:ArrayList 的设计哲学
- 核心思想:通过 动态数组 实现高效随机访问,以 1.5倍扩容 平衡内存与性能。
- 适用场景:优先选择
ArrayList
,除非需要频繁的中间增删操作。 - 最佳实践:预分配容量、避免并发修改、合理使用
trimToSize()
。