深度揭秘:Java LinkedList 源码级使用原理剖析
一、引言
在 Java 的世界里,数据结构是构建高效程序的基石。LinkedList
作为 Java 集合框架中一个重要的成员,以其独特的链式存储结构,在众多场景中发挥着不可替代的作用。与 ArrayList
基于数组的实现方式不同,LinkedList
采用链表结构,这使得它在插入和删除操作上具有显著的优势。本文将深入 Java LinkedList
的源码,详细剖析其内部实现机制和使用原理,带领读者领略这个数据结构的魅力。
二、LinkedList 概述
2.1 基本概念
LinkedList
是 Java 集合框架中的一个双向链表实现的列表类,它实现了 List
和 Deque
接口。这意味着 LinkedList
不仅可以作为普通的列表使用,还能当作队列(Queue)和栈(Stack)来使用。双向链表的每个节点都包含指向前一个节点和后一个节点的引用,这种结构使得 LinkedList
在链表的任意位置进行插入和删除操作都非常高效。
2.2 继承关系与接口实现
下面是 LinkedList
类的定义以及它的继承关系和接口实现:
java
// LinkedList 类继承自 AbstractSequentialList 类,并实现了 List、Deque、Cloneable 和 Serializable 接口
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 内部代码省略
}
AbstractSequentialList
:提供了List
接口的部分实现,主要针对顺序访问的列表。List
:表示LinkedList
是一个列表,可以存储一组有序的元素。Deque
:双端队列接口,意味着LinkedList
可以在两端进行插入和删除操作。Cloneable
:表示LinkedList
支持克隆操作。Serializable
:表示LinkedList
可以进行序列化,方便在网络传输或持久化存储。
三、LinkedList 的内部结构
3.1 节点类 Node
LinkedList
内部使用一个静态内部类 Node
来表示链表中的每个节点。以下是 Node
类的源码:
java
// 静态内部类 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;
}
}
从上述代码可以看出,每个 Node
节点包含三个属性:
item
:存储节点实际的数据元素。next
:指向下一个节点的引用,如果是链表的最后一个节点,next
为null
。prev
:指向前一个节点的引用,如果是链表的第一个节点,prev
为null
。
3.2 链表的头尾节点和大小
LinkedList
类中定义了两个重要的属性 first
和 last
,分别表示链表的头节点和尾节点,以及一个 size
属性表示链表中元素的数量。以下是相关源码:
java
// 链表的头节点
transient Node<E> first;
// 链表的尾节点
transient Node<E> last;
// 链表中元素的数量
transient int size = 0;
first
:指向链表的第一个节点,如果链表为空,first
为null
。last
:指向链表的最后一个节点,如果链表为空,last
为null
。size
:记录链表中元素的数量,初始值为 0。
3.3 构造函数
LinkedList
提供了两个构造函数,一个是无参构造函数,另一个是可以接收一个集合作为参数的构造函数。以下是构造函数的源码:
java
// 无参构造函数,创建一个空的 LinkedList
public LinkedList() {
}
// 构造函数,接收一个集合作为参数,将集合中的元素添加到 LinkedList 中
public LinkedList(Collection<? extends E> c) {
// 调用无参构造函数
this();
// 调用 addAll 方法将集合中的元素添加到链表中
addAll(c);
}
- 无参构造函数:创建一个空的
LinkedList
,此时first
和last
都为null
,size
为 0。 - 带集合参数的构造函数:先调用无参构造函数创建一个空的链表,然后调用
addAll
方法将集合中的元素依次添加到链表中。
四、基本操作的源码分析
4.1 添加元素
4.1.1 add(E e) 方法
add(E e)
方法用于在链表的尾部添加一个元素。以下是该方法的源码:
java
// 在链表的尾部添加一个元素
public boolean add(E e) {
// 调用 linkLast 方法将元素添加到链表的尾部
linkLast(e);
return true;
}
// 将元素添加到链表的尾部
void linkLast(E e) {
// 获取当前的尾节点
final Node<E> l = last;
// 创建一个新的节点,新节点的前一个节点为当前尾节点,元素为 e,下一个节点为 null
final Node<E> newNode = new Node<>(l, e, null);
// 将尾节点更新为新节点
last = newNode;
if (l == null) {
// 如果当前尾节点为 null,说明链表为空,将头节点也更新为新节点
first = newNode;
} else {
// 否则,将原尾节点的下一个节点指向新节点
l.next = newNode;
}
// 链表元素数量加 1
size++;
// 记录链表结构修改次数,用于迭代器的并发修改检查
modCount++;
}
add(E e)
方法调用linkLast
方法将元素添加到链表的尾部。linkLast
方法的步骤如下:- 获取当前的尾节点
l
。 - 创建一个新的节点
newNode
,其前一个节点为l
,元素为e
,下一个节点为null
。 - 将尾节点更新为
newNode
。 - 如果
l
为null
,说明链表为空,将头节点也更新为newNode
。 - 否则,将原尾节点的
next
引用指向newNode
。 - 链表元素数量
size
加 1。 - 增加
modCount
,用于迭代器的并发修改检查。
- 获取当前的尾节点
4.1.2 add(int index, E element) 方法
add(int index, E element)
方法用于在指定位置插入一个元素。以下是该方法的源码:
java
// 在指定位置插入一个元素
public void add(int index, E element) {
// 检查索引是否越界
checkPositionIndex(index);
if (index == size) {
// 如果索引等于链表的大小,说明要在尾部添加元素,调用 linkLast 方法
linkLast(element);
} else {
// 否则,调用 linkBefore 方法在指定位置的前一个节点之后插入元素
linkBefore(element, node(index));
}
}
// 检查索引是否越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index)) {
// 如果索引越界,抛出 IndexOutOfBoundsException 异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 判断索引是否在有效范围内
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 获取指定索引位置的节点
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;
}
}
// 在指定节点之前插入一个元素
void linkBefore(E e, Node<E> succ) {
// 获取指定节点的前一个节点
final Node<E> pred = succ.prev;
// 创建一个新的节点,新节点的前一个节点为 pred,元素为 e,下一个节点为 succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 将指定节点的前一个节点更新为新节点
succ.prev = newNode;
if (pred == null) {
// 如果指定节点的前一个节点为 null,说明指定节点是头节点,将头节点更新为新节点
first = newNode;
} else {
// 否则,将原前一个节点的下一个节点指向新节点
pred.next = newNode;
}
// 链表元素数量加 1
size++;
// 记录链表结构修改次数,用于迭代器的并发修改检查
modCount++;
}
add(int index, E element)
方法的步骤如下:- 调用
checkPositionIndex
方法检查索引是否越界。 - 如果索引等于链表的大小,调用
linkLast
方法在尾部添加元素。 - 否则,调用
node(index)
方法获取指定索引位置的节点,然后调用linkBefore
方法在该节点之前插入元素。
- 调用
node(int index)
方法会根据索引的位置,决定是从链表头部还是尾部开始遍历,以提高查找效率。linkBefore
方法的步骤如下:- 获取指定节点的前一个节点
pred
。 - 创建一个新的节点
newNode
,其前一个节点为pred
,元素为e
,下一个节点为succ
。 - 将指定节点的
prev
引用指向newNode
。 - 如果
pred
为null
,说明指定节点是头节点,将头节点更新为newNode
。 - 否则,将
pred
的next
引用指向newNode
。 - 链表元素数量
size
加 1。 - 增加
modCount
,用于迭代器的并发修改检查。
- 获取指定节点的前一个节点
4.1.3 addAll(Collection<? extends E> c) 方法
addAll(Collection<? extends E> c)
方法用于将一个集合中的所有元素添加到链表的尾部。以下是该方法的源码:
java
// 将一个集合中的所有元素添加到链表的尾部
public boolean addAll(Collection<? extends E> c) {
// 调用 addAll 方法,从链表的尾部开始添加元素
return addAll(size, c);
}
// 在指定位置插入一个集合中的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
// 检查索引是否越界
checkPositionIndex(index);
// 将集合转换为数组
Object[] a = c.toArray();
// 获取数组的长度
int numNew = a.length;
if (numNew == 0) {
// 如果数组长度为 0,说明集合为空,返回 false
return false;
}
// 定义前驱节点和后继节点
Node<E> pred, succ;
if (index == size) {
// 如果索引等于链表的大小,说明要在尾部添加元素,后继节点为 null,前驱节点为当前尾节点
succ = null;
pred = last;
} else {
// 否则,获取指定索引位置的节点作为后继节点,前驱节点为后继节点的前一个节点
succ = node(index);
pred = succ.prev;
}
// 遍历数组,将数组中的元素依次添加到链表中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 创建一个新的节点,新节点的前一个节点为 pred,元素为 e,下一个节点为 null
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null) {
// 如果前驱节点为 null,说明新节点是头节点,将头节点更新为新节点
first = newNode;
} else {
// 否则,将前驱节点的下一个节点指向新节点
pred.next = newNode;
}
// 将前驱节点更新为新节点
pred = newNode;
}
if (succ == null) {
// 如果后继节点为 null,说明是在尾部添加元素,将尾节点更新为最后一个新节点
last = pred;
} else {
// 否则,将最后一个新节点的下一个节点指向后继节点,后继节点的前一个节点指向最后一个新节点
pred.next = succ;
succ.prev = pred;
}
// 更新链表元素数量
size += numNew;
// 记录链表结构修改次数,用于迭代器的并发修改检查
modCount++;
return true;
}
addAll(Collection<? extends E> c)
方法调用addAll(size, c)
方法,从链表的尾部开始添加元素。addAll(int index, Collection<? extends E> c)
方法的步骤如下:- 调用
checkPositionIndex
方法检查索引是否越界。 - 将集合转换为数组
a
。 - 如果数组长度为 0,返回
false
。 - 根据索引位置确定前驱节点
pred
和后继节点succ
。 - 遍历数组
a
,依次创建新节点并插入到链表中。 - 如果
succ
为null
,说明是在尾部添加元素,将尾节点更新为最后一个新节点。 - 否则,将最后一个新节点的
next
引用指向succ
,succ
的prev
引用指向最后一个新节点。 - 更新链表元素数量
size
。 - 增加
modCount
,用于迭代器的并发修改检查。
- 调用
4.2 删除元素
4.2.1 remove() 方法
remove()
方法用于删除链表的第一个元素。以下是该方法的源码:
java
// 删除链表的第一个元素
public E remove() {
// 调用 removeFirst 方法删除第一个元素
return removeFirst();
}
// 删除链表的第一个元素
public E removeFirst() {
// 获取当前的头节点
final Node<E> f = first;
if (f == null) {
// 如果头节点为 null,说明链表为空,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
// 调用 unlinkFirst 方法删除头节点
return unlinkFirst(f);
}
// 删除头节点
private E unlinkFirst(Node<E> f) {
// 获取头节点的元素
final E element = f.item;
// 获取头节点的下一个节点
final Node<E> next = f.next;
// 将头节点的元素置为 null
f.item = null;
// 将头节点的下一个节点置为 null
f.next = null; // help GC
// 将头节点更新为下一个节点
first = next;
if (next == null) {
// 如果下一个节点为 null,说明链表只有一个节点,将尾节点也置为 null
last = null;
} else {
// 否则,将新头节点的前一个节点置为 null
next.prev = null;
}
// 链表元素数量减 1
size--;
// 记录链表结构修改次数,用于迭代器的并发修改检查
modCount++;
return element;
}
remove()
方法调用removeFirst
方法删除链表的第一个元素。removeFirst
方法的步骤如下:- 获取当前的头节点
f
。 - 如果
f
为null
,说明链表为空,抛出NoSuchElementException
异常。 - 调用
unlinkFirst
方法删除头节点。
- 获取当前的头节点
unlinkFirst
方法的步骤如下:- 获取头节点的元素
element
。 - 获取头节点的下一个节点
next
。 - 将头节点的元素和下一个节点引用置为
null
,帮助垃圾回收。 - 将头节点更新为
next
。 - 如果
next
为null
,说明链表只有一个节点,将尾节点也置为null
。 - 否则,将
next
的prev
引用置为null
。 - 链表元素数量
size
减 1。 - 增加
modCount
,用于迭代器的并发修改检查。
- 获取头节点的元素
4.2.2 remove(int index) 方法
remove(int index)
方法用于删除指定位置的元素。以下是该方法的源码:
java
// 删除指定位置的元素
public E remove(int index) {
// 检查索引是否越界
checkElementIndex(index);
// 调用 node 方法获取指定索引位置的节点
return unlink(node(index));
}
// 检查元素索引是否越界
private void checkElementIndex(int index) {
if (!isElementIndex(index)) {
// 如果索引越界,抛出 IndexOutOfBoundsException 异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 判断元素索引是否在有效范围内
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
// 删除指定节点
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) {
// 如果前一个节点为 null,说明指定节点是头节点,将头节点更新为下一个节点
first = next;
} else {
// 否则,将前一个节点的下一个节点指向指定节点的下一个节点
prev.next = next;
// 将指定节点的前一个节点引用置为 null
x.prev = null;
}
if (next == null) {
// 如果下一个节点为 null,说明指定节点是尾节点,将尾节点更新为前一个节点
last = prev;
} else {
// 否则,将下一个节点的前一个节点指向指定节点的前一个节点
next.prev = prev;
// 将指定节点的下一个节点引用置为 null
x.next = null;
}
// 将指定节点的元素置为 null
x.item = null;
// 链表元素数量减 1
size--;
// 记录链表结构修改次数,用于迭代器的并发修改检查
modCount++;
return element;
}
remove(int index)
方法的步骤如下:- 调用
checkElementIndex
方法检查索引是否越界。 - 调用
node(index)
方法获取指定索引位置的节点。 - 调用
unlink
方法删除该节点。
- 调用
unlink
方法的步骤如下:- 获取指定节点的元素
element
。 - 获取指定节点的下一个节点
next
和前一个节点prev
。 - 如果
prev
为null
,说明指定节点是头节点,将头节点更新为next
。 - 否则,将
prev
的next
引用指向next
,并将指定节点的prev
引用置为null
。 - 如果
next
为null
,说明指定节点是尾节点,将尾节点更新为prev
。 - 否则,将
next
的prev
引用指向prev
,并将指定节点的next
引用置为null
。 - 将指定节点的元素置为
null
。 - 链表元素数量
size
减 1。 - 增加
modCount
,用于迭代器的并发修改检查。
- 获取指定节点的元素
4.2.3 remove(Object o) 方法
remove(Object o)
方法用于删除链表中第一个匹配的元素。以下是该方法的源码:
java
// 删除链表中第一个匹配的元素
public boolean remove(Object o) {
if (o == null) {
// 如果要删除的元素为 null
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
// 找到第一个元素为 null 的节点,调用 unlink 方法删除该节点
unlink(x);
return true;
}
}
} else {
// 如果要删除的元素不为 null
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
// 找到第一个匹配的节点,调用 unlink 方法删除该节点
unlink(x);
return true;
}
}
}
return false;
}
remove(Object o)
方法的步骤如下:- 如果要删除的元素
o
为null
,遍历链表,找到第一个元素为null
的节点,调用unlink
方法删除该节点。 - 如果要删除的元素
o
不为null
,遍历链表,找到第一个匹配的节点,调用unlink
方法删除该节点。 - 如果找到并删除了元素,返回
true
;否则,返回false
。
- 如果要删除的元素
4.3 获取元素
4.3.1 get(int index) 方法
get(int index)
方法用于获取指定位置的元素。以下是该方法的源码:
java
// 获取指定位置的元素
public E get(int index) {
// 检查索引是否越界
checkElementIndex(index);
// 调用 node 方法获取指定索引位置的节点,并返回其元素
return node(index).item;
}
get(int index)
方法的步骤如下:- 调用
checkElementIndex
方法检查索引是否越界。 - 调用
node(index)
方法获取指定索引位置的节点。 - 返回该节点的元素。
- 调用
4.3.2 getFirst() 方法
getFirst()
方法用于获取链表的第一个元素。以下是该方法的源码:
java
// 获取链表的第一个元素
public E getFirst() {
// 获取当前的头节点
final Node<E> f = first;
if (f == null) {
// 如果头节点为 null,说明链表为空,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
// 返回头节点的元素
return f.item;
}
getFirst()
方法的步骤如下:- 获取当前的头节点
f
。 - 如果
f
为null
,说明链表为空,抛出NoSuchElementException
异常。 - 返回
f
的元素。
- 获取当前的头节点
4.3.3 getLast() 方法
getLast()
方法用于获取链表的最后一个元素。以下是该方法的源码:
java
// 获取链表的最后一个元素
public E getLast() {
// 获取当前的尾节点
final Node<E> l = last;
if (l == null) {
// 如果尾节点为 null,说明链表为空,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
// 返回尾节点的元素
return l.item;
}
getLast()
方法的步骤如下:- 获取当前的尾节点
l
。 - 如果
l
为null
,说明链表为空,抛出NoSuchElementException
异常。 - 返回
l
的元素。
- 获取当前的尾节点
4.4 修改元素
4.4.1 set(int index, E element) 方法
set(int index, E element)
方法用于修改指定位置的元素。以下是该方法的源码:
java
// 修改指定位置的元素
public E set(int index, E element) {
// 检查索引是否越界
checkElementIndex(index);
// 调用 node 方法获取指定索引位置的节点
Node<E> x = node(index);
// 获取该节点原来的元素
E oldVal = x.item;
// 将该节点的元素更新为新元素
x.item = element;
// 返回原来的元素
return oldVal;
}
set(int index, E element)
方法的步骤如下:- 调用
checkElementIndex
方法检查索引是否越界。 - 调用
node(index)
方法获取指定索引位置的节点x
。 - 获取
x
原来的元素oldVal
。 - 将
x
的元素更新为element
。 - 返回
oldVal
。
- 调用
五、迭代器的实现
5.1 迭代器接口
LinkedList
实现了 Iterable
接口,因此可以使用 iterator()
方法获取一个迭代器来遍历链表。以下是 iterator()
方法的源码:
java
// 获取一个迭代器
public Iterator<E> iterator() {
// 返回一个 ListItr 对象,从链表的第一个元素开始迭代
return new ListItr(0);
}
// 内部类 ListItr 实现了 Iterator 接口,用于迭代链表
private class ListItr implements ListIterator<E> {
// 上一次返回的节点
private Node<E> lastReturned;
// 下一个要返回的节点
private Node<E> next;
// 下一个要返回的节点的索引
private int nextIndex;
// 记录创建迭代器时链表的修改次数,用于并发修改检查
private int expectedModCount = modCount;
// 构造函数,从指定索引位置开始迭代
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size)? null : node(index);
nextIndex = index;
}
// 判断是否还有下一个元素
public boolean hasNext() {
return nextIndex < size;
}
// 获取下一个元素
public E next() {
// 检查是否有并发修改
checkForComodification();
if (!hasNext()) {
// 如果没有下一个元素,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
// 判断是否还有前一个元素
public boolean hasPrevious() {
return nextIndex > 0;
}
// 获取前一个元素
public E previous() {
// 检查是否有并发修改
checkForComodification();
if (!hasPrevious()) {
// 如果没有前一个元素,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
lastReturned = next = (next == null)? last : next.prev;
nextIndex--;
return lastReturned.item;
}
// 获取下一个元素的索引
public int nextIndex() {
return nextIndex;
}
// 获取前一个元素的索引
public int previousIndex() {
return nextIndex - 1;
}
// 删除当前迭代的元素
public void remove() {
// 检查是否有并发修改
checkForComodification();
if (lastReturned == null) {
// 如果上一次返回的节点为 null,抛出 IllegalStateException 异常
throw new IllegalStateException();
}
Node<E> lastNext = lastReturned.next;
// 调用 unlink 方法删除上一次返回的节点
unlink(lastReturned);
if (next == lastReturned) {
next = lastNext;
} else {
nextIndex--;
}
lastReturned = null;
expectedModCount++;
}
// 修改当前迭代的元素
public void set(E e) {
if (lastReturned == null) {
// 如果上一次返回的节点为 null,抛出 IllegalStateException 异常
throw new IllegalStateException();
}
// 检查是否有并发修改
checkForComodification();
lastReturned.item = e;
}
// 在当前迭代位置插入一个元素
public void add(E e) {
// 检查是否有并发修改
checkForComodification();
lastReturned = null;
if (next == null) {
// 如果下一个节点为 null,说明是在尾部添加元素,调用 linkLast 方法
linkLast(e);
} else {
// 否则,调用 linkBefore 方法在当前位置插入元素
linkBefore(e, next);
}
nextIndex++;
expectedModCount++;
}
// 检查是否有并发修改
final void checkForComodification() {
if (modCount != expectedModCount) {
// 如果链表的修改次数与创建迭代器时记录的修改次数不一致,抛出 ConcurrentModificationException 异常
throw new ConcurrentModificationException();
}
}
}
iterator()
方法返回一个ListItr
对象,从链表的第一个元素开始迭代。ListItr
类实现了ListIterator
接口,提供了以下方法:hasNext()
:判断是否还有下一个元素。next()
:获取下一个元素。hasPrevious()
:判断是否还有前一个元素。previous()
:获取前一个元素。nextIndex()
:获取下一个元素的索引。previousIndex()
:获取前一个元素的索引。remove()
:删除当前迭代的元素。set(E e)
:修改当前迭代的元素。add(E e)
:在当前迭代位置插入一个元素。checkForComodification()
:检查是否有并发修改,如果链表的修改次数与创建迭代器时记录的修改次数不一致,抛出ConcurrentModificationException
异常。
5.2 逆序迭代器
LinkedList
还提供了 descendingIterator()
方法,用于获取一个逆序迭代器。以下是该方法的源码:
java
// 获取一个逆序迭代器
public Iterator<E> descendingIterator() {
// 返回一个 DescendingIterator 对象
return new DescendingIterator();
}
// 内部类 DescendingIterator 实现了 Iterator 接口,用于逆序迭代链表
private class DescendingIterator implements Iterator<E> {
// 获取一个 ListItr 对象,从链表的最后一个元素开始迭代
private final ListItr itr = new ListItr(size());
// 判断是否还有下一个元素(逆序)
public boolean hasNext() {
return itr.hasPrevious();
}
// 获取下一个元素(逆序)
public E next() {
return itr.previous();
}
// 删除当前迭代的元素
public void remove() {
itr.remove();
}
}
descendingIterator()
方法返回一个DescendingIterator
对象。DescendingIterator
类实现了Iterator
接口,通过调用ListItr
的hasPrevious()
和previous()
方法实现逆序迭代。
六、作为队列和栈的使用
6.1 作为队列使用
LinkedList
实现了 Deque
接口,因此可以作为队列(Queue)使用。队列是一种先进先出(FIFO)的数据结构,LinkedList
提供了以下方法来实现队列的操作:
6.1.1 offer(E e) 方法
offer(E e)
方法用于在队列的尾部添加一个元素。以下是该方法的源码:
java
// 在队列的尾部添加一个元素
public boolean offer(E e) {
// 调用 add 方法在链表的尾部添加元素
return add(e);
}
offer(E e)
方法调用add(e)
方法在链表的尾部添加元素。
6.1.2 poll() 方法
poll()
方法用于移除并返回队列的头部元素。以下是该方法的源码:
java
// 移除并返回队列的头部元素
public E poll() {
// 获取当前的头节点
final Node<E> f = first;
return (f == null)? null : unlinkFirst(f);
}
poll()
方法的步骤如下:- 获取当前的头节点
f
。 - 如果
f
为null
,说明队列为空,返回null
。 - 否则,调用
unlinkFirst
方法删除头节点并返回其元素。
- 获取当前的头节点
6.1.3 peek() 方法
peek()
方法用于返回队列的头部元素,但不移除该元素。以下是该方法的源码:
java
// 返回队列的头部元素,但不移除该元素
public E peek() {
// 获取当前的头节点
final Node<E> f = first;
return (f == null)? null : f.item;
}
peek()
方法的步骤如下:
6.1.3 peek() 方法
peek()
方法用于返回队列的头部元素,但不移除该元素。以下是该方法的源码:
java
// 返回队列的头部元素,但不移除该元素
public E peek() {
// 获取当前的头节点
final Node<E> f = first;
// 如果头节点为空,说明队列为空,返回null
return (f == null)? null : f.item;
}
该方法逻辑非常简单,仅判断头节点 first
是否为 null
,若为空则表明队列为空,返回 null
;若不为空,直接返回头节点存储的元素 f.item
,此操作不会改变链表结构 。
6.1.4 队列操作总结
通过上述方法,LinkedList
实现了队列的基本功能:
- 入队操作 :
offer(E e)
方法通过调用add(e)
,在链表尾部添加元素,符合队列"先进后出"的入队规则。 - 出队操作 :
poll()
方法移除并返回头节点元素,实现了队列头部元素的取出,维护了队列的先进先出特性。 - 查看队首元素 :
peek()
方法允许开发者在不改变队列结构的情况下获取队首元素,方便进行队列状态的检查 。
6.2 作为栈使用
LinkedList
同样可以作为栈(Stack)使用,栈是一种后进先出(LIFO)的数据结构 。LinkedList
利用其实现的 Deque
接口方法,实现栈相关操作:
6.2.1 push(E e) 方法
push(E e)
方法用于将元素压入栈顶,以下是该方法的源码:
java
// 将元素压入栈顶
public void push(E e) {
// 调用addFirst方法在链表头部添加元素
addFirst(e);
}
push(E e)
方法直接调用 addFirst(e)
,在链表头部插入新元素。因为栈的特性是后进先出,将新元素插入头部,使其成为新的栈顶元素,后续取出元素时也是从头部开始,符合栈的操作逻辑 。
6.2.2 pop() 方法
pop()
方法用于移除并返回栈顶元素,以下是该方法的源码:
java
// 移除并返回栈顶元素
public E pop() {
// 调用removeFirst方法移除并返回链表头部元素
return removeFirst();
}
pop()
方法通过调用 removeFirst()
,移除链表头部元素并返回。由于头部元素是最后压入栈的(后进),移除头部元素即实现了栈顶元素的弹出,遵循栈的后进先出规则 。
6.2.3 栈操作总结
通过 push(E e)
和 pop()
方法,LinkedList
实现了栈的基本功能:
- 压栈操作 :
push(E e)
方法将元素添加到链表头部,使其成为栈顶元素。 - 弹栈操作 :
pop()
方法移除并返回链表头部元素,实现了栈顶元素的弹出 。此外,结合peek()
方法(在作为栈使用时可用于查看栈顶元素),LinkedList
提供了完整的栈操作支持 。
七、其他重要方法分析
7.1 contains(Object o) 方法
contains(Object o)
方法用于判断链表中是否包含指定元素,以下是该方法的源码:
java
// 判断链表中是否包含指定元素
public boolean contains(Object o) {
// 遍历链表,查找指定元素
return indexOf(o) != -1;
}
// 返回指定元素在链表中第一次出现的索引,若不存在则返回-1
public int indexOf(Object o) {
int index = 0;
// 如果要查找的元素为null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
return index;
}
index++;
}
} else {
// 如果要查找的元素不为null
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
return index;
}
index++;
}
}
return -1;
}
contains(Object o)
方法本质上调用了 indexOf(o)
方法。indexOf(o)
方法通过遍历链表,分情况(元素为 null
和不为 null
)查找指定元素 。一旦找到匹配元素,立即返回其索引;若遍历完整个链表都未找到,则返回 -1
。contains(Object o)
方法根据 indexOf(o)
的返回值是否为 -1
,判断链表是否包含指定元素 。这种实现方式虽然简单直接,但在链表长度较大时,遍历操作可能导致性能下降 。
7.2 size() 方法
size()
方法用于返回链表中元素的数量,以下是该方法的源码:
java
// 返回链表中元素的数量
public int size() {
return size;
}
LinkedList
内部维护了一个 size
变量,在每次添加、删除元素的操作中都会对其进行相应的增减 。因此,size()
方法直接返回 size
变量的值,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,能快速获取链表元素数量 。
7.3 clear() 方法
clear()
方法用于清空链表,移除所有元素,以下是该方法的源码:
java
// 清空链表,移除所有元素
public void clear() {
// 遍历链表,将每个节点的元素、前后节点引用置为null
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
// 更新头节点和尾节点为null
first = last = null;
// 重置元素数量为0
size = 0;
// 记录链表结构修改次数
modCount++;
}
clear()
方法通过遍历链表,将每个节点的 item
、next
和 prev
引用都置为 null
,帮助垃圾回收机制回收节点内存 。遍历完成后,将 first
和 last
置为 null
,表示链表为空,并将 size
重置为 0
,同时增加 modCount
记录结构修改 。这种方式确保了链表被彻底清空,释放了占用的内存资源 。
八、性能分析
8.1 时间复杂度分析
- 插入操作 :
- 在链表尾部插入元素(
add(E e)
):由于LinkedList
维护了尾节点引用,直接在尾部插入新节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 。 - 在指定位置插入元素(
add(int index, E element)
):需要先找到指定位置的节点,平均需要遍历链表一半的节点(node(index)
方法),时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为链表长度;找到节点后插入操作本身时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,因此整体插入操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
- 在链表尾部插入元素(
- 删除操作 :
- 删除头部元素(
removeFirst()
):直接操作头节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 。 - 删除指定位置元素(
remove(int index)
):同样需要先找到指定位置的节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,找到后删除操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,整体删除操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。 - 删除指定元素(
remove(Object o)
):需要遍历链表查找元素,最坏情况下遍历整个链表,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
- 删除头部元素(
- 查找操作 :
- 获取指定位置元素(
get(int index)
):查找指定位置节点时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,获取元素本身时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,整体时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。 - 判断元素是否存在(
contains(Object o)
):需要遍历链表查找元素,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
- 获取指定位置元素(
8.2 空间复杂度分析
LinkedList
每个节点存储元素以及前后节点引用,空间占用与元素数量成正比 。其空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为链表中元素的数量 。与基于数组实现的 ArrayList
相比,LinkedList
不需要预先分配固定大小的内存空间,但每个节点额外的引用会占用一定内存 。
8.3 与 ArrayList 的性能对比
- 随机访问 :
ArrayList
基于数组实现,通过索引直接访问元素,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ;LinkedList
需要遍历链表,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,因此在随机访问场景下ArrayList
性能更优 。 - 插入和删除 :在链表中间或头部插入、删除元素时,
LinkedList
只需修改节点引用,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) (不考虑查找节点的时间);而ArrayList
可能需要移动大量元素,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。但在数组尾部插入元素时,ArrayList
平均时间复杂度接近 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) (摊还分析) 。因此,频繁在中间或头部进行插入、删除操作时,LinkedList
性能更好;而在尾部插入以及随机访问较多的场景下,ArrayList
更具优势 。
九、总结与展望
9.1 总结
通过深入分析 Java LinkedList
的源码,我们全面了解了其使用原理。LinkedList
采用双向链表结构,通过 Node
节点类存储元素和前后节点引用,实现了高效的插入和删除操作 。它不仅作为普通列表使用,还通过实现 Deque
接口,支持队列和栈的操作 。在方法实现上,添加、删除、获取等操作都围绕链表节点的引用修改和遍历展开 。同时,其迭代器的实现保证了遍历过程中的并发安全检查 。
在性能方面,LinkedList
在插入和删除操作(尤其是在链表头部或中间)上具有优势,但随机访问性能较差 。这使得它适用于需要频繁插入、删除元素,而随机访问需求较少的场景,如实现队列、栈数据结构,或者处理数据频繁变动的链表数据 。
9.2 展望
随着 Java 技术的不断发展,未来 LinkedList
可能会在性能优化和功能扩展上有所改进 。例如,在查找性能优化方面,可能会引入更高效的查找算法或数据结构辅助,减少遍历操作带来的性能损耗 。在并发处理上,或许会进一步增强对多线程环境的支持,提供更高效、安全的并发操作方式 。此外,结合新的 Java 特性,如 Java 8 引入的流操作、Java 11 的 HTTP 客户端等,LinkedList
可能会在功能上进行拓展,更好地适应复杂多变的开发需求 。
对于开发者而言,深入理解 LinkedList
的原理和特性,有助于在实际开发中根据具体场景选择合适的数据结构,编写出更高效、健壮的代码 。