Java 中的 ArrayList 和 LinkedList 是两种常用的列表实现,
它们在底层结构、性能特性和适用场景上有显著差异。
一、底层数据结构
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 数据结构 | 动态数组(可自动扩容) | 双向链表(每个节点包含前驱和后继指针) |
| 内存布局 | 连续内存空间存储元素 | 非连续内存,通过指针关联元素 |
| 存储单元 | 数组元素直接存储对象引用 | 节点对象(Node)存储数据和指针 |
二、性能特性对比
1. 随机访问效率
-
ArrayList:O(1)
通过数组下标直接访问元素,无需遍历。 -
LinkedList:O(n)
需要从头 / 尾节点开始遍历到目标位置,平均时间复杂度为 O (n/2)。
示例场景:
// ArrayList 随机访问
ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("A", "B", "C"));
System.out.println(arrayList.get(1)); // 直接定位,O(1)
// LinkedList 随机访问
LinkedList<String> linkedList = new LinkedList<>(Arrays.asList("A", "B", "C"));
System.out.println(linkedList.get(1)); // 从头节点遍历,O(n)
2. 插入 / 删除效率
-
ArrayList:
- 尾部插入:平均 O (1)(扩容时 O (n))。
- 中间 / 头部插入:O (n)(需移动后续元素)。
-
LinkedList:
-
指定位置插入 / 删除:O (n)(需先定位到目标位置)。
-
头部 / 尾部插入 / 删除:O (1)(直接操作头尾节点)。
-
示例场景:
// ArrayList 中间插入
arrayList.add(1, "X"); // 需移动后续元素,O(n)
// LinkedList 头部插入
linkedList.addFirst("X"); // 修改头指针,O(1)
3. 内存占用
-
ArrayList:
- 主要开销是数组预分配空间(可能存在空间浪费)。
- 空 ArrayList 默认容量为 10,扩容因子为 1.5 倍。
-
LinkedList:
- 每个节点需额外存储前驱和后继指针,空间利用率较低。
- 元素越多,指针占用的额外空间占比越大。
三、适用场景推荐
| 场景 | ArrayList 更适合 | LinkedList 更适合 |
|---|---|---|
| 频繁随机访问元素 | ✅(O (1) 效率) | ❌(O (n) 效率) |
| 频繁在尾部插入 / 删除元素 | ✅(尾部操作接近 O (1)) | ✅(O (1) 效率) |
| 频繁在头部或中间插入 / 删除 | ❌(需移动元素,O (n)) | ✅(定位后 O (1) 操作) |
| 存储大量元素且空间敏感 | ✅(无额外指针开销) | ❌(指针占用额外空间) |
| 需实现栈或队列功能 | ❌(尾部操作但扩容可能浪费) | ✅(Deque 接口实现) |
四、常见误区与注意事项
-
插入效率的误解:
-
很多人认为 LinkedList 的插入效率一定优于 ArrayList,但实际需分情况:
- 若插入位置已知(如头部 / 尾部),LinkedList 更高效。
- 若需先通过下标定位(如
add(5, element)),ArrayList 可能更高效(因定位 O (1) vs O (n))。
-
-
扩容机制的影响:
- ArrayList 扩容需复制数组,大数据量下可能影响性能。若初始容量预估合理(如
new ArrayList<>(1000)),可减少扩容次数。
- ArrayList 扩容需复制数组,大数据量下可能影响性能。若初始容量预估合理(如
-
遍历方式的选择:
-
ArrayList 推荐使用普通 for 循环(随机访问 O (1))。
-
LinkedList 强制使用迭代器或 foreach(本质是迭代器),避免随机访问。
-
错误示例:
java
// 错误:用普通 for 循环遍历 LinkedList
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i)); // 每次 get(i) 都需从头遍历,O(n²)
}
// 正确:用迭代器遍历 LinkedList
for (String element : linkedList) {
System.out.println(element); // O(n)
}
五、性能测试对比
以下是一个简单的性能测试代码,对比 ArrayList 和 LinkedList 在不同操作下的耗时:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ListPerformanceTest {
public static void main(String[] args) {
int size = 100000;
// 测试随机访问
testRandomAccess(size);
// 测试头部插入
testAddFirst(size);
// 测试尾部插入
testAddLast(size);
}
private static void testRandomAccess(int size) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 填充数据
for (int i = 0; i < size; i++) {
arrayList.add(i);
linkedList.add(i);
}
// 测试随机访问
long startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
arrayList.get(i);
}
long endTime = System.nanoTime();
System.out.println("ArrayList 随机访问耗时: " + (endTime - startTime) / 1_000_000 + " ms");
startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
linkedList.get(i);
}
endTime = System.nanoTime();
System.out.println("LinkedList 随机访问耗时: " + (endTime - startTime) / 1_000_000 + " ms");
}
private static void testAddFirst(int size) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 测试头部插入
long startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
arrayList.add(0, i);
}
long endTime = System.nanoTime();
System.out.println("ArrayList 头部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
linkedList.addFirst(i);
}
endTime = System.nanoTime();
System.out.println("LinkedList 头部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
}
private static void testAddLast(int size) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 测试尾部插入
long startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
arrayList.add(i);
}
long endTime = System.nanoTime();
System.out.println("ArrayList 尾部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
startTime = System.nanoTime();
for (int i = 0; i < size; i++) {
linkedList.addLast(i);
}
endTime = System.nanoTime();
System.out.println("LinkedList 尾部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
}
}
典型测试结果(数据量 10 万) :
ArrayList 随机访问耗时: 3 ms
LinkedList 随机访问耗时: 5367 ms
ArrayList 头部插入耗时: 1524 ms
LinkedList 头部插入耗时: 5 ms
ArrayList 尾部插入耗时: 11 ms
LinkedList 尾部插入耗时: 13 ms
六、总结与选择建议
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 随机访问 | 极高效(O (1)) | 低效(O (n)) |
| 插入 / 删除 | 尾部高效,其他位置低效 | 头尾高效,其他位置需先定位 |
| 内存占用 | 无额外开销,可能预分配空间 | 每个节点需额外两个指针 |
| 适用场景 | 读多写少,随机访问频繁 | 头尾操作频繁,需实现队列 / 栈 |
在实际开发中,若无特殊需求,优先使用 ArrayList,因其更符合常见场景。仅当明确需要频繁的头部 / 中间操作时,才考虑使用 LinkedList。