Java ArrayList详解
Java 中的 ArrayList 是 List 接口最常用的实现类,底层基于动态数组实现,支持自动扩容,是开发中存储有序、可重复数据的首选容器。本文从底层原理、核心方法、性能特点到最佳实践,全面解析 ArrayList。
一、ArrayList 本质:动态数组的 “智能封装”
数组是 Java 中最基础的线性结构,但长度固定(一旦创建无法修改)。ArrayList 对数组进行了封装,核心解决了 “数组长度固定” 的痛点 —— 当元素数量超过当前容量时,会自动创建更大的新数组,将旧元素复制过去,实现 “动态扩容”。
简单说:ArrayList = 数组 + 自动扩容机制。
二、底层结构与核心参数
1. 核心成员变量(JDK 1.8)
public class ArrayList
// 存储元素的底层数组(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
// 方法2:指定初始容量(推荐!减少扩容次数)
List
// 方法3:通过其他集合初始化
List
2. 增(add)
List
// 尾部添加(常用)
list.add("苹果"); // [苹果]
list.add("香蕉"); // [苹果, 香蕉]
// 指定索引添加(插入到中间)
list.add(1, "橙子"); // [苹果, 橙子, 香蕉](原索引1及之后元素后移)
// 批量添加(添加另一个集合的所有元素)
list.addAll(Arrays.asList("葡萄", "西瓜")); // [苹果, 橙子, 香蕉, 葡萄, 西瓜]
注意:指定索引添加(add(int index, E e))会导致该索引后的元素集体后移(时间复杂度 O(n)),频繁在中间插入元素效率低。
3. 删(remove)
List
// 按索引删除(返回被删除元素)
String removed = list.remove(1); // 删除索引1的元素(B),结果:[A, C, D]
// 按元素删除(删除第一个匹配的元素,返回是否删除成功)
boolean isRemoved = list.remove("C"); // 结果:[A, D],返回true
注意:
按索引删除后,该索引后的元素会集体前移(时间复杂度 O(n)),频繁删除中间元素效率低。
遍历中删除元素需谨慎:用增强 for 循环删除会抛出 ConcurrentModificationException(快速失败机制),推荐用迭代器的 remove() 方法:
Iterator
while (it.hasNext()) {
String s = it.next();
if (s.equals("A")) {
it.remove(); // 迭代器删除,安全
}
}
4. 改(set)
List
list.set(1, "X"); // 将索引1的元素改为X,结果:[A, X, C]
时间复杂度 O(1):直接通过索引修改数组元素,高效。
5. 查(get、contains、indexOf)
List
// 按索引查(高效)
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
// 方法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
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
// 线程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
用 CopyOnWriteArrayList(并发容器,适合读多写少场景):
List
六、最佳实践(避坑指南)
初始化时指定容量:若已知元素数量(如预计存储 1000 条数据),创建时指定容量 new ArrayList<>(1000),可避免多次扩容(扩容的数组复制操作耗时)。
避免在循环中使用 remove(int index):循环删除会导致索引偏移(删除后元素前移),可能漏删或越界。例如:
// 错误示例:删除所有偶数,会漏删
List
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
// List
七、与 LinkedList 的核心区别
特性ArrayListLinkedList
底层结构
动态数组
双向链表
查询效率(get)
高(O (1))
低(O (n))
中间增删效率
低(O (n),需移动元素)
高(O (1),只需改指针)
内存占用
较少(连续空间)
较多(每个节点存前后指针)
适合场景
频繁查询、尾部增删
频繁中间增删
总结
ArrayList 是基于动态数组的 List 实现,核心优势是按索引查询高效(O(1)),适合频繁读取数据的场景。其底层扩容机制(1.5 倍扩容)和数组特性,决定了 “中间增删效率低” 的特点。
posted on
2025-09-08 09:08
coding博客
阅读(48)
评论(0)
收藏
举报