LinkedList介绍
LinkedList
特点如下:
- 基于双向链表实现 :
LinkedList
的实现数据结构是双向链表,链表由一系列Node节点组成,每个Node包含数据、指向前一个Node的引用、指向后一个Node的引用。链表的大小是动态的,可以根据需要增加或减少节点。 - 高效的插入和删除:在链表中插入或删除元素通常只需要改变相关节点的引用,这使得操作效率较高。
- 访问效率低 :与数组不同,链表不支持随机访问。获取链表中特定位置的元素需要从头/尾开始遍历链表直到找到目标位置,时间复杂度为
O(n)
,效率较低。 - 额外的内存开销 :由于每个节点需要存储指向前一个节点和后一个节点的引用,因此相比基于数组的实现(
ArrayList
),链表会消耗更多的内存。 - 不需要连续的内存空间:链表不像数组那样需要一块连续的内存空间,它可以利用分散的内存区域存储数据。
LinkedList
的类继承结构如下:

- 继承自
AbstractSequentialList
:AbstractSequentialList
要求子类必须覆盖的方法包括listIterator()
和size()
。其中listIterator()
方法用于返回列表迭代器,允许对列表进行遍历、添加、删除和修改等操作;而size()
方法则需要返回列表中元素的数量。AbstractSequentialList
通过依赖列表迭代器实现对列表的操作,为适合顺序访问的列表数据结构提供了一个简化实现的抽象框架。因此,继承自AbstractSequentialList
的类更适合顺序访问,而非随机访问。 - 实现了
List<E>
接口 :List
接口定义了允许重复元素的有序集合(即列表)操作,,支持根据索引进行元素访问、添加、删除等操作。 - 实现了 Deque 接口 :表示
LinkedList
能够在两端 高效地添加和移除元素,既可以用作队列 也可以用作栈。 - 实现了
Cloneable
接口 :表示支持对象的克隆操作。LinkedList
提供了clone()
方法来创建当前列表的一个浅拷贝。 - 实现了
Serializable
接口 :表示LinkedList
支持序列化操作,可用于对象的持久化存储或网络传输。
LinkedList源码分析
以 jdk 1.8
版本为例,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;
}
Node节点结构:
java
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;
}
}
双向链表结构图示:
构造链表
java
/**
* 默认构造函数:构造空链表,就是什么都不做
*/
public LinkedList() {
}
/**
* 根据指定集合构造链表(顺序由集合的Iterator提供)
*/
public LinkedList(Collection<? extends E> c) {
this();
// 把集合c所有元素逐个插入链表中
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
// 从size位置开始(即链表尾部开始),插入集合c的所有元素
return addAll(size, c);
}
/**
* 从下标为的index的地方开始,把指定集合c中的所有元素插入链表
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 检查index是否越界(在 [0,size] 闭区间内),若越界则抛出IndexOutOfBoundsException异常
checkPositionIndex(index);
// 集合转为数组,方便逐个操作
Object[] a = c.toArray();
// 待添加元素的数量
int numNew = a.length;
// 若待添加元素数量为0,则不增加,并返回false
if (numNew == 0)
return false;
// 找到待插入位置(index位置)的前驱与后继节点
Node<E> pred, succ;
if (index == size) {
// 若在链表尾部追加数据:
// 后继节点是null
succ = null;
// 前驱节点是当前的尾节点
pred = last;
} else {
// 后继节点是当前index位置的节点
succ = node(index);
// 前驱节点是当前index位置的前驱节点
pred = succ.prev;
}
// for循环遍历待添加元素的数组,依次执行插入节点操作
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 构造新节点
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
// 前驱节点为空,说明是头节点
first = newNode;
else
// 更新前驱节点的后继节点(实现节点"插入")
pred.next = newNode;
// 更新前驱节点
pred = newNode;
}
// 循环插入新节点结束后
if (succ == null) {
// 若后继节点为空,说明是尾节点,则设置尾节点
last = pred;
} else {
// 前驱和后继节点需要链接起来
pred.next = succ;
succ.prev = pred;
}
// 更新size
size += numNew;
// 更新modeCount
modCount++;
return true;
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
* 遍历链表找到index位置的节点
*/
Node<E> node(int index) {
// 若 index < size/2,就从位置0往后遍历到位置index处
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 若 index >= size/2,就从位置size往前遍历到位置index处
else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
操作方法
插入节点

java
/**
* 在指定位置插入一个元素
*/
public void add(int index, E element) {
// 检查index,越界则抛出 IndexOutOfBoundsException 异常
checkPositionIndex(index);
if (index == size)
// 在尾部插入
linkLast(element);
else
// 在中间插入:找到当前 index 位置节点,在其前面插入
linkBefore(element, node(index));
}
/**
* 在链表尾部插入一个元素
*/
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
size++;
// 更新modCount
modCount++;
}
/**
* 在指定节点的前面插入一个节点
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 记录index节点的前驱节点
final Node<E> pred = succ.prev;
// 创建新节点
final Node<E> newNode = new Node<>(pred, e, succ);
// index节点的前驱节点指向新节点
succ.prev = newNode;
if (pred == null)
// index节点的原前驱节点为null,说明原节点是头节点,则头节点指向新节点
first = newNode;
else
// index节点的原前驱节点的后继节点指向新节点
pred.next = newNode;
// 更新size
size++;
// 更新modCount
modCount++;
}
/**
* 遍历链表找到index位置的节点
*/
Node<E> node(int index) {
// 若 index < size/2,就从位置0往后遍历到位置index处
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 若 index >= size/2,就从位置size往前遍历到位置index处
else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
删除节点

java
/**
* 根据指定索引删除元素
*/
public E remove(int index) {
// 检查index是否越界,若是则抛出 IndexOutOfBoundsException 异常
checkElementIndex(index);
// 遍历链表找到指定位置节点,然后移除
return unlink(node(index));
}
/**
* 从链表中移除指定节点
*/
E unlink(Node<E> x) {
// assert x != null;
// 记录待移除节点的数据、后继节点、前驱节点
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;
// 待移除节点的前驱节点置空,方便GC回收
x.prev = null;
}
if (next == null) {
// 后继节点为空,说明待移除节点是尾节点,则尾节点指向为待移除节点的前驱节点
last = prev;
} else {
// 后继节点的前驱节点指向待移除节点的前驱节点
next.prev = prev;
// 待移除节点的后继节点置空,方便GC回收
x.next = null;
}
// 待移除节点的元素值置空,方便GC回收
x.item = null;
// 更新size
size--;
// 更新modCount
modCount++;
// 返回移除节点的数据
return element;
}
/**
* 遍历链表找到index位置的节点
*/
Node<E> node(int index) {
// 若 index < size/2,就从位置0往后遍历到位置index处
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 若 index >= size/2,就从位置size往前遍历到位置index处
else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
查询节点
java
/**
* 获取指定位置数据
*/
public E get(int index) {
// 检查index是否越界,若越界则抛出 IndexOutOfBoundsException 异常
checkElementIndex(index);
// 遍历链表找到index位置节点,返回节点中的数据
return node(index).item;
}
/**
* 遍历链表找到index位置的节点
*/
Node<E> node(int index) {
// 若 index < size/2,就从位置0往后遍历到位置index处
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 若 index >= size/2,就从位置size往前遍历到位置index处
else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
实现Deque接口的方法
实现 Deque
接口的方法,可以用作双向队列 、栈。
java
/**
* 队头入队
*/
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* 队尾入队
*/
public boolean offerLast(E e) {
addLast(e);
return true;
}
/**
* 获取队头数据
*/
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
* 获取队尾数据
*/
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
/**
* 队头出队
*/
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 队尾出队
*/
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
/**
* 入栈
*/
public void push(E e) {
addFirst(e);
}
/**
* 出栈
*/
public E pop() {
return removeFirst();
}
核心原理总结
LinkedList
实现了一个双向链表 的数据结构。链表中的每个Node
包含三个成员变量:前驱节点(prev
)、后继节点(next
)和存储的元素(item
)。双向链表的成员变量中存储了链表的头结点和尾节点。链表节点的增删主要就是操作节点的前驱和后继引用实现。LinkedList
实现了Deque
接口,支持在链表两端进行高效地插入和删除操作,因此也可以作为栈、队列和双端队列来使用。LinkedList
插入和删除操作非常高效(尤其是两端的操作),时间复杂度为O(1)
。访问特定索引的元素效率较低,因为需要从一端开始遍历到指定位置,平均时间复杂度为O(n)
。因此,LinkedList
在处理需要频繁插入和删除操作的应用场景中表现优异,但在随机访问方面则不如基于数组的集合类型(如ArrayList
)高效。- 链表中是没有下标索引的,若要找到指定位置的元素,就必须要遍历链表。源码中对链表的遍历实现了优化:先将指定位置
index
与链表长度size
的一半比较,如果index<size/2
,则从位置 0 往后遍历到位置index
处;如果index>size/2
,则从位置size
往前遍历到位置index
处,从而提高遍历效率(当然,实际上遍历效率还是很低)。