前言
在 Java 集合框架中,ArrayList
和 LinkedList
是两种最常用的 List
实现类,它们都用于存储和操作有序集合。尽管它们都实现了 List
接口,但在底层实现、性能特点和适用场景上存在显著差异。理解这些差异对于编写高效、健壮的代码至关重要。本文将从底层结构、性能、内存占用和适用场景等方面对 ArrayList
和 LinkedList
进行详细对比,帮助开发者在实际开发中做出更合理的选择。
一、核心区别概览
特性 | ArrayList | LinkedList |
---|---|---|
底层数据结构 | 动态数组 | 双向链表 |
内存占用 | 更紧凑(仅存储数据) | 更高(每个元素带两个指针) |
随机访问性能 | O(1) - 极快 | O(n) - 较慢 |
头部插入/删除 | O(n) - 需要移动元素 | O(1) - 直接修改指针 |
尾部插入/删除 | O(1) - 快速(摊销时间) | O(1) - 快速 |
中间插入/删除 | O(n) - 需要移动元素 | O(n) - 需要遍历到位置 |
迭代器性能 | 快速 | 快速 |
内存局部性 | 优秀(连续内存) | 差(元素分散在内存中) |
二、底层数据结构详解
1. ArrayList
arduino
// 内部实现
transient Object[] elementData; // 存储元素的数组
private int size; // 当前元素数量
- 基于动态可扩展数组
- 初始容量通常为10
- 当容量不足时自动扩容(通常是原容量的1.5倍)
源码:类属性
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//序列号版本号
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//空数组,指定默认初始化容量为0时赋值给elementData,避免了重复创建空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//当调用无参构造方法时,赋值给elementData,主要用于在添加第一个元素前,标记该ArrayList是由无参构造器创建,便于将容量初始化为DEFAULT_CAPACITY,避免了重复创建空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//实际存放元素的数组
transient Object[] elementData; // non-private to simplify nested class access
//ArrayList的元素个数
private int size;
}
ArrayList
是List接口的实现类,底层基于数组实现,容量可根据需要动态增加,相当于动态数组。ArrayList
继承于AbstractList
,并且还实现了Cloneable
、Serializable
、RandomAccess
接口。
List
:表明是列表数据结构,可以通过下标对元素进行添加删除或查找。Serializable
:表示可以进行序列化和反序列化操作,可以把对象与字节流相互转换。RandomAccess
:有这个接口标记的List表示可以支持快速随机访问,即通过元素下标可以直接得到元素内容。Cloneable
:表示支持拷贝,可以通过浅拷贝或深拷贝来复制对象的副本。
2. LinkedList
java
// 内部节点结构
private static class Node<E> {
E item; // 存储的数据
Node<E> next; // 指向下一个节点
Node<E> prev; // 指向上一个节点
}
// 链表属性
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
transient int size = 0; // 元素数量
- 基于双向链表实现
- 每个元素都包装在节点对象中
- 节点包含前后指针(增加内存开销)
三、性能对比详解
1. 访问元素(get 操作)
scss
// ArrayList 直接通过索引访问数组
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index); // 直接数组访问
}
// LinkedList 需要遍历链表
public E get(int index) {
checkElementIndex(index);
return node(index).item; // 需要遍历到指定位置
}
- ArrayList:O(1) 时间复杂度,直接通过数组索引访问
- LinkedList:O(n) 时间复杂度,平均需要遍历半个列表
2. 插入/删除操作
尾部操作
typescript
// ArrayList 尾部添加
public boolean add(E e) {
modCount++;
add(e, elementData, size); // 通常为O(1),扩容时O(n)
return true;
}
// LinkedList 尾部添加
public boolean add(E e) {
linkLast(e); // 总是O(1)
return true;
}
- 两者在尾部操作都很高效(O(1))
- ArrayList 在需要扩容时有临时性能下降
头部操作
ini
// ArrayList 头部插入
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
// 需要移动所有后续元素
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
}
// LinkedList 头部插入
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
}
- ArrayList:O(n) - 需要移动所有后续元素
- LinkedList:O(1) - 只需修改几个指针
3. 内存占用
-
ArrayList:
- 每个元素占用约 4 字节(引用大小)
- 少量额外空间(size 计数等)
- 可能有未使用的预留空间
-
LinkedList:
- 每个元素需要额外 24 字节(12字节对象头 + 前后指针各4字节 + 4字节数据引用)
- 内存分散,缓存不友好
4. 迭代性能对比
ini
// 遍历 100,000 个元素的时间对比
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 填充数据...
long start = System.nanoTime();
for (int i : arrayList) { /* 遍历操作 */ }
long arrayTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i : linkedList) { /* 遍历操作 */ }
long linkedTime = System.nanoTime() - start;
System.out.println("ArrayList遍历时间: " + arrayTime + "ns");
System.out.println("LinkedList遍历时间: " + linkedTime + "ns");
- 两者迭代性能接近(O(n))
- ArrayList 通常更快(更好的内存局部性)
- LinkedList 在迭代时删除元素更高效
使用场景推荐
使用 ArrayList 当:
✅ 需要频繁随机访问元素(按索引) ✅ 主要在列表尾部添加/删除元素 ✅ 内存资源紧张 ✅ 需要最小化迭代开销 ✅ 元素数量相对稳定
使用 LinkedList 当:
✅ 需要频繁在头部插入/删除元素 ✅ 需要实现队列或双端队列功能 ✅ 列表中间需要频繁插入/删除 ✅ 内存资源充足 ✅ 主要使用迭代器进行遍历和修改
最佳实践
-
初始化容量(ArrayList):
arduino// 预估大小以减少扩容次数 List<String> list = new ArrayList<>(1000);
-
使用迭代器删除(LinkedList):
iniIterator<Integer> iter = linkedList.iterator(); while (iter.hasNext()) { if (iter.next() % 2 == 0) { iter.remove(); // O(1) 操作 } }
-
避免使用索引循环(LinkedList):
scss// 差 - O(n^2) 性能 for (int i = 0; i < linkedList.size(); i++) { linkedList.get(i); // 每次都是O(n) } // 好 - O(n) 性能 for (Integer num : linkedList) { // 使用增强for循环 }
-
考虑替代方案:
arduino// 需要频繁头部操作时 Deque<String> deque = new ArrayDeque<>(); // 比LinkedList更高效 // 需要线程安全时 List<String> safeList = Collections.synchronizedList(new ArrayList<>());
总结
维度 | ArrayList 优势 | LinkedList 优势 |
---|---|---|
随机访问 | ⭐⭐⭐⭐⭐ 极快 | ⭐ 很慢 |
头部操作 | ⭐ 很慢 | ⭐⭐⭐⭐⭐ 极快 |
内存效率 | ⭐⭐⭐⭐ 较高效 | ⭐ 较低效 |
迭代性能 | ⭐⭐⭐⭐ 很快 | ⭐⭐⭐ 较快 |
中间修改 | ⭐⭐ 较慢 | ⭐⭐⭐ 较快(使用迭代器) |
简单决策指南:
- 90% 的情况选择 ArrayList(Java 标准库和大多数框架的默认选择)
- 需要实现队列/栈功能时选择 LinkedList 或 ArrayDeque
- 在极端性能敏感场景进行基准测试
总的来说,ArrayList
和 LinkedList
各有优缺点,适用的场景也有所不同。选择哪个集合类应根据具体需求来决定。如果你需要频繁进行随机访问,ArrayList
是更好的选择;而如果你需要频繁进行插入和删除操作,尤其是在列表的头部或尾部,LinkedList
会更合适。
在实际编程中,理解它们的区别并根据需求合理选择数据结构,可以大大提高程序的性能和效率。希望本文能够帮助大家更好地理解这两种常见的 Java 集合类,为编写高效、灵活的代码提供参考。