在日常开发中,List 是最常用的数据结构之一。而 ArrayList 与 LinkedList 虽然都实现了 List 接口,但背后的实现机制与使用场景却有本质区别。本文从底层结构、增删查改效率、源码原理到实战建议,全方位拆解两者的差异。
一、底层结构:一个是数组,一个是链表
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 Object[] | 双向链表 |
内存布局 | 连续内存,访问效率高 | 非连续内存,插入删除效率高 |
数据访问方式 | 通过索引随机访问 | 通过节点指针顺序访问 |
二、增删查改的性能对比(时间复杂度)
操作 | ArrayList | LinkedList |
---|---|---|
增加尾部 | O(1)(均摊) | O(1) |
增加中间 | O(n)(元素需后移) | O(n)(遍历定位) |
删除中间 | O(n)(元素需前移) | O(n)(遍历定位) |
查找(get) | O(1) | O(n) |
🧠 关键总结:
-
ArrayList:读写频繁、随机访问性能更佳;
-
LinkedList:适用于频繁插入/删除(尤其头尾);
三、源码实现深度解析
我们从以下关键维度对 ArrayList 与 LinkedList 的源码展开剖析:
3.1 ArrayList 源码剖析
初始化逻辑
// 无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
-
初始并不立即分配数组,真正添加第一个元素时才触发 grow() 扩容逻辑;
-
默认初始容量是 10(在第一次添加时设置);
-
elementData 是一个 Object[],数组存储的是对象引用。
添加元素逻辑
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e;
return true;
}
ensureCapacityInternal 源码:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 扩容
}
grow 扩容算法:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
🧠 关键点:
-
增长比例为原始容量的 1.5 倍;
-
System.arraycopy() 是主要的性能开销来源;
-
每次扩容都会创建新数组并复制数据,GC 压力明显。
删除元素逻辑
public E remove(int index) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
elementData[--size] = null; // help GC
return oldValue;
}
📌 删除中间元素时:
-
数组整体后移;
-
null 出被移除的元素,避免内存泄漏;
-
大数据量删除操作是性能瓶颈。
3.2 LinkedList 源码剖析
核心结构定义
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
transient Node<E> first;
transient Node<E> last;
-
双向链表结构,支持从头尾快速插入;
-
遍历必须从 first 或 last 逐步定位。
添加元素(尾部)逻辑
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
📌 插入尾部仅需 3 步:
-
创建新节点;
-
更新前一个 last 的 next;
-
重置 last 指针。
查找元素逻辑
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// 从靠近 index 的一端开始查找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
性能影响:
-
任意位置访问需要线性查找;
-
会优先选择离 index 最近的方向遍历;
-
对大规模数据结构的 get(i) 不推荐。
删除元素逻辑
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null; // help GC
size--;
modCount++;
return element;
}
删除优势:
-
不需要整体移动结构,只改指针;
-
整体复杂度为 O(1),前提是节点已知。
3.3 modCount 与 fail-fast 机制
两者都维护 modCount 字段,用于结构修改次数计数。
配合 Iterator 实现 fail-fast 机制:
public class Itr implements Iterator<E> {
int expectedModCount = modCount;
public E next() {
checkForComodification(); // 结构变化会抛出异常
}
}
-
并发修改会触发 ConcurrentModificationException;
-
不适合在多线程环境下直接使用,需加锁或使用并发集合。
3.4 小结对比
维度 | ArrayList | LinkedList |
---|---|---|
结构 | 动态数组 | 双向链表 |
插入效率 | 尾部快,中间慢(需数组移动) | 中间快(只改指针),但需遍历定位 |
访问效率 | 随机访问快,O(1) | 随机访问慢,O(n) |
内存 | 连续空间、空间紧凑 | 非连续空间,内存碎片大 |
扩容 | 自动扩容 + 数组复制(高 GC 压力) | 插入不扩容,但频繁 new 对象 |
四、内存与 GC 行为差异
-
ArrayList 内存连续,局部性好,CPU 缓存命中率高;
-
LinkedList 每个节点单独分配内存,占用更多堆空间;
-
GC 行为上,链表节点回收复杂度高于数组,可能频繁触发 Minor GC。
五、线程安全性说明
默认情况下,两者 都不是线程安全;
如需线程安全:
-
可使用 Collections.synchronizedList(list) 封装;
-
或者使用 CopyOnWriteArrayList(适合读多写少);
六、实战场景选型建议
场景 | 推荐使用 |
---|---|
频繁随机访问 | ArrayList |
数据量大 + 尾部频繁添加 | ArrayList |
插入/删除频繁(尤其头尾) | LinkedList |
内存敏感、对局部性要求高 | ArrayList |
需要遍历/排序操作较多 | ArrayList 更高效 |
七、误用警告 ⚠️
❌ LinkedList 不适合做栈:虽然有 addFirst()/removeFirst(),但在并发或复杂逻辑下容易出错;
❌ 不要使用 LinkedList 遍历查找元素:效率远低于数组,尤其是大数据量时;
❌ 不要在 ArrayList 删除时用普通 for 循环,建议使用倒序删除或 Iterator 进行安全操作。
八、结合 Java 8+ Stream API 使用注意事项
-
ArrayList 遍历性能优越,配合 parallelStream() 更适合并行流;
-
LinkedList 遍历开销大,不推荐用于流操作(尤其是 map/filter 处理时);
九、结语
虽然 ArrayList 和 LinkedList 都是 List 接口的实现,但差异远比想象中大。了解其底层结构与行为差异,才能写出高性能、低风险的 Java 代码。不要“用着都能跑”就觉得两者无所谓,关键时刻,选对集合能救命!