LinkedList 底层实现与 ArrayList 对比分析
目录
一、LinkedList 的底层数据结构:双向链表
1. 节点(Node)的结构
2. 双向链表的整体结构
二、LinkedList 的核心特性
1. 增删效率高:无需移动元素,仅修改指针
示例 1:尾部添加元素(add(E e))
示例 2:指定位置删除元素(remove(int index))
2. 无需扩容:内存利用率更高
3. 实现多接口:支持队列 / 栈操作
4. 线程不安全:与 ArrayList 一致
三、LinkedList vs ArrayList:核心差异对比
四、LinkedList 的使用
五、总结
一、LinkedList 的底层数据结构:双向链表
LinkedList
的底层是双向链表(Doubly Linked List),这是它与ArrayList
(动态数组)最本质的区别。链表结构不依赖连续内存空间,而是通过 “节点间的引用” 串联元素,每个节点(Node
)包含三个部分:
1. 节点(Node)的结构
LinkedList
内部定义了一个私有的Node
类,作为链表的基本单元,源码如下:
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. 双向链表的整体结构
LinkedList
通过两个指针(first
和last
)维护整个链表的首尾节点,无需像数组那样记录 “容量”,只需跟踪实际元素个数(size
):
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient Node<E> first; // 指向链表的头节点transient Node<E> last; // 指向链表的尾节点transient int size = 0; // 实际元素个数
}
双向链表的结构特点:
- 每个节点都能通过
prev
和next
找到前后节点,支持 “双向遍历”; - 链表的首尾节点特殊:头节点的
prev
为null
,尾节点的next
为null
; - 新增 / 删除节点时,只需修改前后节点的引用(指针),无需移动大量元素。
二、LinkedList 的核心特性
基于双向链表的结构,LinkedList
呈现出与ArrayList
完全不同的特性,核心优势集中在 “增删操作” 上:
1. 增删效率高:无需移动元素,仅修改指针
无论是在链表的 “头部、尾部还是中间位置” 增删元素,LinkedList
只需修改对应节点的prev
和next
引用,时间复杂度为O(1)(前提是已知目标节点位置)。
示例 1:尾部添加元素(add(E e)
)
直接通过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); // 新建节点,前驱为l,后继为nulllast = newNode; // 新节点成为新的尾节点if (l == null) // 若原链表为空,新节点同时也是头节点first = newNode;elsel.next = newNode; // 原尾节点的后继指向新节点size++;
}
示例 2:指定位置删除元素(remove(int index)
)
先通过node(index)
方法找到目标节点,再修改前后节点的引用:
public E remove(int index) {checkElementIndex(index); // 检查索引合法性return unlink(node(index)); // 找到节点并删除
}// 找到指定索引的节点(优化:根据索引位置选择从头或从尾遍历,提升效率)
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;}
}// 删除目标节点:修改前后节点的引用,断开目标节点的连接
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--;return element;
}
2. 无需扩容:内存利用率更高
LinkedList
的元素存储依赖节点,每个节点按需创建(添加元素时新建节点,删除元素时回收节点),无需像ArrayList
那样 “提前预留容量” 或 “扩容时复制数组”,内存利用率更高,不会产生 “空闲容量浪费”。
3. 实现多接口:支持队列 / 栈操作
LinkedList
不仅实现了List
接口,还实现了Deque
接口(双端队列),因此可以作为队列(FIFO) 或栈(LIFO) 使用,提供了丰富的操作方法:
- 队列操作:
offer()
(尾部入队)、poll()
(头部出队)、peek()
(获取头部元素); - 栈操作:
push()
(头部入栈)、pop()
(头部出栈)、peek()
(获取栈顶元素)。
4. 线程不安全:与 ArrayList 一致
和ArrayList
一样,LinkedList
的所有方法都没有加锁,多线程环境下同时修改会导致数据不一致(如节点引用错乱)。若需线程安全,可使用Collections.synchronizedList(new LinkedList<>())
或CopyOnWriteArrayList
(后者更适合读多写少场景)。
三、LinkedList vs ArrayList:核心差异对比
对比维度 | LinkedList(双向链表) | ArrayList(动态数组) |
---|---|---|
底层结构 | 双向链表(节点 + 指针) | 动态数组(Object []) |
随机访问(get (index)) | 效率低(需遍历链表,O (n)) | 效率高(索引直接访问,O (1)) |
增删操作 | 效率高(仅修改指针,O (1),已知节点时) | 效率低(需移动元素,O (n),非尾部时) |
尾部增删(add/removeLast) | 效率高(O (1)) | 效率高(O (1),无扩容时);扩容时 O (n) |
内存占用 | 内存开销大(每个节点存 prev/next 引用) | 内存开销小(仅存元素,可能有空闲容量) |
扩容机制 | 无需扩容(按需创建节点) | 自动扩容(默认 1.5 倍,需复制数组) |
遍历方式 | 推荐迭代器 / 增强 for(避免随机访问) | 推荐随机访问 / 增强 for / 迭代器 |
适用场景 | 频繁增删(尤其是中间位置)、队列 / 栈 | 频繁查询(随机访问)、少增删 |
四、LinkedList 的使用
-
优先用于 “频繁增删” 场景:如任务队列、消息队列等需要频繁添加 / 删除元素的场景,避免在 “频繁查询” 场景中使用(如商品列表展示,需频繁通过索引获取元素)。
-
避免随机访问(get (index)):若需频繁通过索引获取元素,果断改用
ArrayList
。LinkedList 的get(index)
方法虽做了优化(根据索引位置选择从头或从尾遍历),但仍需 O (n) 时间,远慢于 ArrayList 的 O (1)。 -
批量添加元素时,无需手动优化:与 ArrayList 不同,LinkedList 添加元素时无需提前 “预留容量”,直接循环
add()
即可,不会产生扩容开销。 -
遍历推荐用迭代器或增强 for:LinkedList 的迭代器(
Iterator
)是 “fail-fast” 机制(遍历中修改会抛ConcurrentModificationException
),且迭代器遍历比普通 for 循环(频繁调用get(index)
)效率高得多。
五、总结
LinkedList 的设计核心是 “双向链表”,它用 “牺牲随机访问效率” 换取 “高效的增删操作”,完美弥补了 ArrayList 在 “频繁增删” 场景中的不足。在实际开发中,选择两者的核心依据是 “业务操作的侧重点”:
- 若以 “查询” 为主,偶尔增删(且多在尾部)→ 选 ArrayList;
- 若以 “增删” 为主(尤其是中间位置),少查询 → 选 LinkedList。