ArrayList 与 LinkedList 源码深度对比解析
前言
本文基于 JDK 1.8 源码,从底层数据结构、核心方法实现、扩容机制、增删改查效率、使用场景 五个维度,对 ArrayList 和 LinkedList 进行全方位对比,方便后续快速回顾和使用。
一、核心定义与继承结构
1. 继承体系
java
复制代码
// ArrayList 继承结构
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
// LinkedList 继承结构
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- 关键区别 :
ArrayList 实现了 RandomAccess 接口(标记接口,代表支持随机访问);
LinkedList 实现了 Deque 接口(代表支持双端队列、栈功能)。
2. 核心定位
ArrayList:动态数组,基于数组实现,支持快速随机访问;
LinkedList:双向链表,基于链表节点实现,增删效率高,无固定容量限制。
二、底层数据结构源码对比
1. ArrayList 底层:Object 数组
java
复制代码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于无参构造)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 【核心】存储数据的底层数组
transient Object[] elementData;
// 集合实际元素个数
private int size;
}
- 本质:连续内存空间的数组,所有操作围绕这个数组展开。
2. LinkedList 底层:双向链表节点
java
复制代码
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
// 链表长度
transient int size = 0;
// 【核心】链表头节点
transient Node<E> first;
// 【核心】链表尾节点
transient Node<E> last;
// 双向链表节点内部类
private static class Node<E> {
E item; // 存储的数据
Node<E> next; // 后继节点
Node<E> prev; // 前驱节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
- 本质:非连续内存空间,每个节点包含「数据、前驱、后继」三个属性。
三、初始化方式源码对比
1. ArrayList 初始化
(1)无参构造(最常用)
java
复制代码
public ArrayList() {
// 初始化为空数组,第一次添加元素时才扩容
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- 懒加载:初始容量为 0,首次添加元素时扩容为默认容量
10。
(2)指定容量构造
java
复制代码
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 直接创建指定长度的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
2. LinkedList 初始化
java
复制代码
// 无参构造(唯一常用构造)
public LinkedList() {}
- 无容量概念:初始化时无节点,添加元素时才创建节点。
四、核心方法源码深度解析
1. 添加元素(add)
(1)ArrayList add 源码
java
复制代码
public boolean add(E e) {
// 1. 检查容量,不足则扩容
ensureCapacityInternal(size + 1);
// 2. 直接赋值到数组末尾
elementData[size++] = e;
return true;
}
// 扩容核心方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 所需容量 > 当前数组长度,触发扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容逻辑:新容量 = 原容量的 1.5 倍
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 等价于 oldCapacity * 1.5
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 数组拷贝,生成新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 添加流程:检查容量 → 扩容(如需) → 数组末尾赋值;
- 缺点 :扩容时需要数组拷贝,消耗性能;中间插入需要移动元素。
(2)LinkedList add 源码
java
复制代码
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++;
}
- 添加流程:创建新节点 → 修改前驱/后继指针;
- 优点 :无需扩容,无数组拷贝,尾部添加效率极高;
- 缺点:中间插入需要先遍历找到目标位置。
2. 根据索引获取元素(get)
(1)ArrayList get 源码
java
复制代码
public E get(int index) {
// 检查索引是否越界
rangeCheck(index);
// 直接通过数组下标访问,O(1)
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
(2)LinkedList get 源码
java
复制代码
public E get(int index) {
// 检查索引越界
checkElementIndex(index);
// 遍历查找节点,返回数据
return node(index).item;
}
// 核心:二分查找优化遍历
Node<E> node(int 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;
}
}
- 时间复杂度 :
O(n),即使二分优化,仍需遍历,随机访问效率极低。
3. 根据索引删除元素(remove)
(1)ArrayList remove 源码
java
复制代码
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 数组拷贝,移动后续元素,填补空位
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 置空最后一个元素,帮助GC
elementData[--size] = null;
return oldValue;
}
- 缺点 :删除非末尾元素,需要移动大量元素,效率低。
(2)LinkedList remove 源码
java
复制代码
public E remove(int index) {
checkElementIndex(index);
// 1. 找到目标节点
return unlink(node(index));
}
// 解除节点引用,核心逻辑
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; // 置空,帮助GC
size--;
modCount++;
return element;
}
- 优点 :仅需修改指针引用,无需移动元素,删除效率高。
五、扩容机制对比
| 集合 |
扩容触发条件 |
扩容规则 |
性能消耗 |
| ArrayList |
数组容量不足 |
新容量 = 原容量 × 1.5 |
高(需数组拷贝,开辟新连续内存) |
| LinkedList |
无扩容机制 |
按需创建节点,无容量限制 |
无 |
- 关键结论 :
ArrayList 有固定容量,必须扩容;LinkedList 无容量上限,天然支持动态添加。
六、核心特性总结表
| 特性 |
ArrayList |
LinkedList |
| 底层结构 |
动态Object数组 |
双向链表 |
| 随机访问 |
支持(O(1)) |
不支持(O(n)) |
| 尾部增删 |
高效(扩容时略低) |
极高(O(1)) |
| 中间增删 |
低效(需移动元素) |
高效(仅改指针) |
| 内存占用 |
连续内存,尾部有空闲空间 |
非连续内存,每个节点多存2个引用 |
| 线程安全 |
非安全 |
非安全 |
| 实现接口 |
RandomAccess |
Deque(双端队列) |
七、使用场景选择
1. 优先使用 ArrayList
- 大量查询、根据索引访问操作;
- 数据频繁尾部添加/删除;
- 内存有限,需要连续存储(节省节点指针的内存开销)。
2. 优先使用 LinkedList
- 大量中间插入/删除操作;
- 需要实现栈、队列、双端队列 (
LinkedList 实现了 Deque 接口);
- 数据量动态极大,无法预估容量(避免
ArrayList 频繁扩容)。
八、关键注意事项
- ArrayList 避坑 :
- 无参构造初始容量为 0,首次添加才扩容为 10;
- 预估数据量时,指定初始容量 ,减少扩容次数(如
new ArrayList<>(100))。
- LinkedList 避坑 :
- 不要用
for 循环遍历(每次 get(i) 都遍历链表),推荐用迭代器/增强for;
- 内存开销比
ArrayList 大(每个节点存储前驱、后继)。
- 两者均非线程安全 ,多线程环境下使用
CopyOnWriteArrayList 或加锁。
总结
- 底层核心 :
ArrayList 是数组(连续内存、随机访问快),LinkedList 是双向链表(非连续内存、增删快);
- 效率核心 :查多用
ArrayList,增删多用 LinkedList;
- 源码关键 :
ArrayList 核心是扩容+数组拷贝 ,LinkedList 核心是节点指针修改。