ArrayList 就像 自动扩容的「动态数组」,底层用数组存数据,但比普通数组更智能——空间不够时自动扩容,中间插入或删除时自动挪位置。用大白话拆解它的核心机制:
一、底层结构:数组 + 扩容策略
- 核心数组:
Object[] elementData,实际存数据的地方。 - 初始容量:默认10(但第一次添加元素时才真正分配空间)。
- 扩容规则:空间不足时,新容量 = 旧容量 × 1.5(比如10 → 15 → 22 ...)。
- 扩容代价:每次扩容都要复制旧数组到新数组,频繁扩容会影响性能(所以最好预估大小提前设置容量)。
二、核心操作原理
1. 添加元素(add())
-
流程:
-
检查当前数组是否已满。
-
如果满了:
- 创建新数组(大小为旧数组的1.5倍)。
- 把旧数组的数据复制到新数组。
-
将新元素放入数组末尾的空位。
-
-
源码片段:
public boolean add(E e) { ensureCapacityInternal(size + 1); // 检查扩容 elementData[size++] = e; // 放入数组末尾 return true; }
2. 中间插入元素(add(index, element))
-
流程:
- 检查扩容。
- 把插入位置后的元素整体右移一位(类似排队时有人插队,后面的人后退)。
- 放入新元素。
-
时间复杂度:O(n)(移动元素效率低,尽量避免频繁中间插入)。
3. 删除元素(remove(index))
-
流程:
- 把删除位置后的元素整体左移一位。
- 数组末尾置空(帮助GC回收)。
-
示例:
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); list.remove(1); // 删除"B" // 删除后数组变成:["A", "C", "D", null]
4. 访问元素(get(index))
-
直接通过下标访问数组:时间复杂度 O(1)(这是 ArrayList 的最大优势)。
String element = list.get(2); // 直接取数组的第3个位置
三、关键源码分析
1. 扩容实现(grow()方法)
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧容量 × 1.5
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity); // 复制旧数组到新数组
}
2. 中间插入的代价
public void add(int index, E element) {
rangeCheckForAdd(index); // 检查索引是否越界
ensureCapacityInternal(size + 1); // 检查扩容
// 把index后的元素右移一位(System.arraycopy)
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
四、优缺点总结
| 优点 | 缺点 |
|---|---|
| 随机访问极快(O(1)) | 中间插入/删除慢(O(n)) |
| 内存连续,缓存友好 | 扩容有性能开销 |
| 代码简单易用 | 非线程安全 |
五、使用建议
- 提前设置容量:如果知道数据量,构造时指定初始容量(
new ArrayList<>(1000)),避免多次扩容。 - 避免频繁中间操作:中间插入/删除多用
LinkedList。 - 多线程场景:用
Collections.synchronizedList包装,或用CopyOnWriteArrayList。
六、经典对比:ArrayList vs 数组
| 对比项 | ArrayList | 数组 |
|---|---|---|
| 容量 | 动态扩容 | 固定长度 |
| 功能 | 自带增删查改方法 | 需手动实现逻辑 |
| 类型安全 | 支持泛型(编译时检查类型) | 需运行时检查类型 |
| 性能 | 扩容和中间操作有开销 | 无额外开销 |
总结口诀:
「ArrayList 动态数组,自动扩容真省心
随机访问快如箭,中间增删慢吞吞
提前设容是王道,多线程要加锁稳!」