本文将从源码角度来详细对比 ArrayList
和 LinkedList
的实现。下面会结合 JDK8/11 源码来拆解:
ArrayList 源码详解
1. 默认参数
private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 默认空数组
transient Object[] elementData; // 存储数据的数组
private int size; // 实际元素个数
- 初始时,
elementData
并不会直接分配长度为 10 的数组,而是用 空数组。 - 真正分配容量在第一次
add()
时发生。
2. 初始化过程
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组
}
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);
}
}
3. 添加元素与扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e;
return true;
}
扩容逻辑
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 倍。
- 如果 1.5 倍仍不足以容纳新元素,则直接使用
minCapacity
。
4. 使用特点
- 随机访问快(O(1),基于数组索引)。
- 插入/删除慢(O(n),需要移动元素)。
- 适合 查多改少 的场景。
LinkedList 源码详解
1. 数据结构定义
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;
}
}
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
- 双向链表结构。
first
指向头结点,last
指向尾结点。
2. 初始化过程
public LinkedList() {}
- 构造函数几乎没有逻辑,不需要预分配空间。
- 容量自适应,随用随建。
3. 添加元素
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++;
}
- 插入元素时,直接在尾部新建节点并链接。
- 不涉及数组扩容。
4. 删除元素
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;
if (next == null)
last = prev;
else
next.prev = prev;
size--;
return element;
}
- 删除时只需修改指针,不会移动其他节点。
- 删除效率 O(1),但前提是已经定位到目标节点。
5. 使用特点
- 插入/删除快(只需修改前后指针)。
- 随机访问慢(O(n),需要从头/尾遍历)。
- 适合 改多查少 的场景。
对比总结
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
默认容量 | 10(懒加载) | 无需容量,节点动态分配 |
扩容方式 | 1.5 倍 | 不需要扩容 |
插入/删除复杂度 | O(n) | O(1)(已知节点)/O(n)(按索引查找) |
随机访问复杂度 | O(1) | O(n) |
内存开销 | 小(连续数组) | 大(存储节点和指针) |
适用场景 | 查多改少 | 改多查少 |
关键数据结构对比
Mermaid 流程图
ArrayList 扩容机制(基于数组)
1. 默认初始化
- 构造函数
// 默认构造函数,不会立刻分配空间
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- 只有在第一次
add()
时,才会初始化为 默认容量 10。
2. 扩容触发点
当 size == elementData.length
时,触发扩容逻辑:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量够用
elementData[size++] = e;
return true;
}
3. 扩容方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量 = 旧容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 核心:数组拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
关键点:
- 扩容增量 = 原容量的 1/2,即 1.5 倍。
- 使用
System.arraycopy
/Arrays.copyOf
拷贝数组,时间复杂度 O(n)。 - 属于 动态数组扩容,对随机访问友好。
LinkedList 扩容机制【追加节点】(基于链表)
1. 数据结构
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;
}
}
- 底层是 双向链表,不需要预分配数组。
2. 添加元素(无需扩容)
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++;
}
3. 特性
-
没有扩容过程,因为链表节点是动态分配的。
-
新增元素时,仅仅创建一个
Node
并挂到链表尾部。 -
时间复杂度:
- 插入/删除:O(1)(前提是已定位到节点)
- 随机访问:O(n)(需要遍历链表)
总结对比
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
默认容量 | 10 | 无容量限制 |
扩容机制 | 1.5 倍扩容 + 数组拷贝 | 不需要扩容,节点动态分配 |
插入删除性能 | O(n)(可能触发扩容+拷贝) | O(1)(若已定位节点) |
随机访问性能 | O(1) | O(n) |
内存使用 | 紧凑,数组结构 | 额外存储 prev/next 指针,开销大 |
也就是说:
- ArrayList 通过 扩容(grow + copy) 来保证容量。
- LinkedList 没有扩容逻辑,直接通过链表节点动态分配内存。