Java ArrayList详解
Java 中的
ArrayList 是 List 接口最常用的实现类,底层基于动态数组实现,支持自动扩容,是开发中存储有序、可重复数据的首选容器。本文从底层原理、核心方法、性能特点到最佳实践,全面解析 ArrayList。
一、ArrayList 本质:动态数组的 “智能封装”
数组是 Java 中最基础的线性结构,但长度固定(一旦创建无法修改)。
ArrayList 对数组进行了封装,核心解决了 “数组长度固定” 的痛点 —— 当元素数量超过当前容量时,会自动创建更大的新数组,将旧元素复制过去,实现 “动态扩容”。简单说:
ArrayList = 数组 + 自动扩容机制。二、底层结构与核心参数
1. 核心成员变量(JDK 1.8)
public class ArrayList<E> extends AbstractList<E> implements List<E> {
// 存储元素的底层数组(transient 表示不参与序列化)
transient Object[] elementData;
// 当前元素数量(size <= capacity)
private int size;
// 默认初始容量(如果不指定,默认创建容量为10的数组)
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于初始化时容量为0的场景)
private static final Object[] EMPTY_ELEMENTDATA = {};
}
elementData:真正存储元素的数组,类型为Object[],支持泛型(编译期检查类型)。size:当前实际元素个数(不是数组长度),例如new ArrayList()初始化后,size=0,elementData长度为 10(默认容量)。
2. 扩容机制(核心!决定性能的关键)
当添加元素导致
size 超过数组容量(elementData.length)时,ArrayList 会自动扩容,步骤如下:- 计算新容量:默认扩容为原容量的 1.5 倍(公式:
newCapacity = oldCapacity + (oldCapacity >> 1),右移 1 位等价于除以 2,向下取整)。 - 创建新数组(长度为新容量)。
- 将旧数组的元素复制到新数组(
System.arraycopy()方法,高效但耗时)。 - 用新数组替换旧数组(
elementData = newElementData)。
示例:
- 初始容量 10,当添加第 11 个元素时,扩容为
10 + 10/2 = 15。 - 当容量为 15,添加第 16 个元素时,扩容为
15 + 15/2 = 22(15/2=7.5,向下取整为 7,15+7=22)。
三、常用方法与代码示例
1. 初始化
// 方法1:默认初始化(容量10)
List<String> list1 = new ArrayList<>();
// 方法2:指定初始容量(推荐!减少扩容次数)
List<Integer> list2 = new ArrayList<>(100); // 初始容量100
// 方法3:通过其他集合初始化
List<String> list3 = new ArrayList<>(Arrays.asList("A", "B", "C"));
2. 增(add)
List<String> list = new ArrayList<>();
// 尾部添加(常用)
list.add("苹果"); // [苹果]
list.add("香蕉"); // [苹果, 香蕉]
// 指定索引添加(插入到中间)
list.add(1, "橙子"); // [苹果, 橙子, 香蕉](原索引1及之后元素后移)
// 批量添加(添加另一个集合的所有元素)
list.addAll(Arrays.asList("葡萄", "西瓜")); // [苹果, 橙子, 香蕉, 葡萄, 西瓜]
注意:指定索引添加(
add(int index, E e))会导致该索引后的元素集体后移(时间复杂度 O(n)),频繁在中间插入元素效率低。3. 删(remove)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// 按索引删除(返回被删除元素)
String removed = list.remove(1); // 删除索引1的元素(B),结果:[A, C, D]
// 按元素删除(删除第一个匹配的元素,返回是否删除成功)
boolean isRemoved = list.remove("C"); // 结果:[A, D],返回true
注意:
- 按索引删除后,该索引后的元素会集体前移(时间复杂度
O(n)),频繁删除中间元素效率低。 - 遍历中删除元素需谨慎:用增强 for 循环删除会抛出
ConcurrentModificationException(快速失败机制),推荐用迭代器的remove()方法:
Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("A")) { it.remove(); // 迭代器删除,安全 } }
4. 改(set)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.set(1, "X"); // 将索引1的元素改为X,结果:[A, X, C]
- 时间复杂度
O(1):直接通过索引修改数组元素,高效。
5. 查(get、contains、indexOf)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "B"));
// 按索引查(高效)
String s = list.get(2); // 返回C
// 判断是否包含元素(需遍历,时间复杂度O(n))
boolean hasB = list.contains("B"); // true
// 查元素第一次出现的索引(遍历)
int firstB = list.indexOf("B"); // 1
// 查元素最后一次出现的索引(遍历)
int lastB = list.lastIndexOf("B"); // 3
get(int index)效率极高(O(1)):直接通过数组索引访问,这是ArrayList的核心优势。
6. 遍历
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 方法1:for循环(按索引,最常用)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方法2:增强for循环(简洁)
for (String s : list) {
System.out.println(s);
}
// 方法3:迭代器(支持遍历中删除)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 方法4:Java 8+ 流式遍历
list.forEach(System.out::println);
四、性能分析:为什么 “查询快、增删中间慢”?
ArrayList 的性能完全由底层数组的特性决定:| 操作 | 时间复杂度 | 原因分析 |
|---|---|---|
get(int index) |
O(1) |
直接通过索引访问数组元素,一步到位。 |
add(E e)(尾部) |
O(1) |
直接在数组末尾添加,无需移动元素(若不触发扩容)。 |
add(int index, E e) |
O(n) |
需移动 index 后的所有元素(后移一位),元素越多,移动成本越高。 |
remove(int index) |
O(n) |
需移动 index 后的所有元素(前移一位),元素越多,移动成本越高。 |
contains(E e) |
O(n) |
需从头遍历数组,逐个比较元素。 |
核心结论:
- 适合频繁查询、少量尾部增删的场景(如数据展示、查询列表)。
- 不适合频繁在中间增删的场景(如实时日志插入中间位置),这类场景优先用
LinkedList。
五、线程安全性:非线程安全的 “坑”
ArrayList 是非线程安全的集合 —— 多线程同时读写时,可能出现数据错乱或 ConcurrentModificationException 异常。示例:两个线程同时添加元素,可能导致元素丢失:
List<Integer> list = new ArrayList<>();
// 线程1添加元素
new Thread(() -> { for (int i = 0; i < 1000; i++) list.add(i); }).start();
// 线程2添加元素
new Thread(() -> { for (int i = 1000; i < 2000; i++) list.add(i); }).start();
// 最终size可能小于2000(数据丢失)
解决方法:
- 用
Collections.synchronizedList()包装(简单但性能一般):List<Integer> safeList = Collections.synchronizedList(new ArrayList<>()); - 用
CopyOnWriteArrayList(并发容器,适合读多写少场景):List<Integer> safeList = new CopyOnWriteArrayList<>();
六、最佳实践(避坑指南)
-
初始化时指定容量:
若已知元素数量(如预计存储 1000 条数据),创建时指定容量new ArrayList<>(1000),可避免多次扩容(扩容的数组复制操作耗时)。 -
避免在循环中使用
remove(int index):
循环删除会导致索引偏移(删除后元素前移),可能漏删或越界。例如:// 错误示例:删除所有偶数,会漏删 List<Integer> list = new ArrayList<>(Arrays.asList(2, 4, 6, 8)); for (int i = 0; i < list.size(); i++) { if (list.get(i) % 2 == 0) { list.remove(i); // 删除后i递增,会跳过下一个元素 } } // 正确做法:倒序删除或用迭代器 for (int i = list.size() - 1; i >= 0; i--) { ... } -
注意
size()与数组长度的区别:size()返回实际元素个数,elementData.length是当前数组容量(size <= 容量)。若需 “瘦身”(释放未使用的空间),可调用trimToSize():list.trimToSize(); // 使容量=size,节省内存 -
泛型避免基本类型:
ArrayList只能存储对象,若要存基本类型(如int),需用包装类(Integer):List<Integer> list = new ArrayList<>(); // 正确 // List<int> list = new ArrayList<>(); // 错误(泛型不支持基本类型)
七、与 LinkedList 的核心区别
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 查询效率(get) | 高(O (1)) | 低(O (n)) |
| 中间增删效率 | 低(O (n),需移动元素) | 高(O (1),只需改指针) |
| 内存占用 | 较少(连续空间) | 较多(每个节点存前后指针) |
| 适合场景 | 频繁查询、尾部增删 | 频繁中间增删 |
总结
ArrayList 是基于动态数组的 List 实现,核心优势是按索引查询高效(O(1)),适合频繁读取数据的场景。其底层扩容机制(1.5 倍扩容)和数组特性,决定了 “中间增删效率低” 的特点。
浙公网安备 33010602011771号