揭秘 Java LinkedList:从源码到原理的深度剖析
一、引言
在 Java 的集合框架中,LinkedList 是一个非常重要且常用的数据结构。它作为一种线性表,以链表的形式存储元素,与 ArrayList 等基于数组实现的集合不同,LinkedList 在插入和删除操作上表现出独特的优势。对于开发者而言,深入理解 LinkedList 的使用原理不仅有助于在实际项目中做出更合适的选择,还能更好地优化代码性能。本文将深入到 LinkedList 的源码层面,详细剖析其内部结构、核心方法的实现原理以及性能特点,为读者呈现一个全面且深入的 LinkedList 解析。
二、LinkedList 概述
2.1 基本概念
LinkedList 是 Java 集合框架中的一个双向链表实现,它实现了 List 和 Deque 接口,这意味着它既可以作为一个普通的列表使用,支持按索引访问元素,又可以作为双端队列使用,支持在队列的两端进行元素的插入和删除操作。双向链表由一系列节点组成,每个节点包含三个部分:指向前一个节点的引用、当前节点存储的数据以及指向后一个节点的引用。这种结构使得 LinkedList 在插入和删除操作上具有较高的效率,尤其是在链表的头部和尾部进行操作时。
2.2 继承关系与接口实现
从类的继承关系和接口实现角度来看,LinkedList 的定义如下:
// 继承自 AbstractSequentialList 类,实现了 List、Deque、Cloneable 和 Serializable 接口
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 类的具体实现将在后续详细分析
}
可以看到,LinkedList 继承自 AbstractSequentialList 类,该类提供了一些基于顺序访问的列表操作的基本实现。同时,它实现了 List 接口,具备列表的基本功能,如按索引访问元素、插入和删除元素等;实现了 Deque 接口,支持双端队列的操作,如在队列的头部和尾部进行插入和删除;实现了 Cloneable 接口,支持对象的克隆操作;实现了 Serializable 接口,支持对象的序列化和反序列化。
2.3 与其他列表的对比
与其他常见的列表实现(如 ArrayList)相比,LinkedList 具有以下特点:
- 存储结构:
ArrayList基于数组实现,元素在内存中是连续存储的;而LinkedList基于双向链表实现,元素在内存中是分散存储的,通过节点之间的引用连接起来。 - 插入和删除操作:在插入和删除操作上,
LinkedList在链表的头部和尾部进行操作的时间复杂度为 ,在中间位置进行操作的时间复杂度为 ;而ArrayList在尾部插入和删除元素的时间复杂度为 ,在中间位置插入和删除元素需要移动大量元素,时间复杂度为 。因此,当需要频繁进行插入和删除操作时,尤其是在链表的头部和尾部进行操作,LinkedList更具优势。 - 随机访问操作:
ArrayList支持随机访问,通过索引可以直接访问元素,时间复杂度为 ;而LinkedList不支持随机访问,需要从头节点或尾节点开始遍历链表,直到找到目标元素,时间复杂度为 。因此,当需要频繁进行随机访问操作时,ArrayList更合适。
三、LinkedList 的内部结构
3.1 核心属性
LinkedList 类的核心属性决定了其数据存储和操作的基本机制,以下是关键属性的源码及注释:
// 链表的大小,即链表中元素的数量
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;
}
}
size:表示链表中元素的数量,每次进行插入或删除操作时,会相应地更新该值。first:指向链表的头节点,如果链表为空,则该值为null。last:指向链表的尾节点,如果链表为空,则该值为null。Node:是一个静态内部类,用于表示链表中的节点。每个节点包含三个属性:item存储当前节点的数据,next指向下一个节点的引用,prev指向前一个节点的引用。
3.2 数据存储结构
LinkedList 基于双向链表实现,链表由一系列节点组成,每个节点通过 next 和 prev 引用连接起来。头节点的 prev 引用为 null,尾节点的 next 引用为 null。这种结构使得 LinkedList 可以方便地在链表的头部和尾部进行插入和删除操作,同时也支持从头部或尾部开始遍历链表。
3.3 初始化过程
LinkedList 的构造函数有两种重载形式,分别是无参构造函数和带初始元素集合的构造函数。以下是无参构造函数的源码及注释:
// 无参构造函数,初始化一个空的 LinkedList
public LinkedList() {
// 无具体操作,此时 first 和 last 都为 null,size 为 0
}
在无参构造函数中,没有进行具体的操作,此时 first 和 last 都为 null,size 为 0,表示链表为空。
带初始元素集合的构造函数的源码及注释如下:
// 带初始元素集合的构造函数,初始化一个包含指定集合元素的 LinkedList
public LinkedList(Collection<? extends E> c) {
// 调用无参构造函数,初始化一个空的 LinkedList
this();
// 将指定集合中的元素添加到 LinkedList 中
addAll(c);
}
在带初始元素集合的构造函数中,首先调用无参构造函数初始化一个空的 LinkedList,然后调用 addAll 方法将指定集合中的元素添加到 LinkedList 中。
四、基本操作的源码分析
4.1 插入操作
4.1.1 add(E e) 方法
add(E e) 方法用于在链表的尾部添加一个元素。源码及注释如下:
// 在链表的尾部添加一个元素
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
// 如果当前尾节点不为 null,将当前尾节点的下一个节点指向新节点
l.next = newNode;
// 链表的大小加 1
size++;
// 记录链表结构修改的次数,用于迭代器的快速失败机制
modCount++;
}
在 add 方法中,调用 linkLast 方法将元素添加到链表的尾部。在 linkLast 方法中:
- 获取当前的尾节点。
- 创建一个新节点,新节点的前一个节点为当前尾节点,元素为
e,下一个节点为null。 - 将新节点设置为尾节点。
- 如果当前尾节点为
null,说明链表为空,将新节点设置为头节点;否则,将当前尾节点的下一个节点指向新节点。 - 链表的大小加 1。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
4.1.2 add(int index, E element) 方法
add(int index, E element) 方法用于在指定索引位置插入一个元素。源码及注释如下:
// 在指定索引位置插入一个元素
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))
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;
// 创建一个新节点,新节点的前一个节点为指定节点的前一个节点,元素为 e,下一个节点为指定节点
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) 方法中:
- 首先检查索引是否越界。
- 如果索引等于链表的大小,说明要在链表的尾部插入元素,调用
linkLast方法。 - 否则,调用
node方法获取指定索引位置的节点,然后调用linkBefore方法在该节点之前插入元素。
在 node 方法中:
- 如果索引小于链表大小的一半,从头部开始遍历链表,直到找到指定索引位置的节点。
- 否则,从尾部开始遍历链表,直到找到指定索引位置的节点。
在 linkBefore 方法中:
- 获取指定节点的前一个节点。
- 创建一个新节点,新节点的前一个节点为指定节点的前一个节点,元素为
e,下一个节点为指定节点。 - 将指定节点的前一个节点指向新节点。
- 如果指定节点的前一个节点为
null,说明指定节点是头节点,将新节点设置为头节点;否则,将指定节点的前一个节点的下一个节点指向新节点。 - 链表的大小加 1。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
4.1.3 addFirst(E e) 方法
addFirst(E e) 方法用于在链表的头部添加一个元素。源码及注释如下:
// 在链表的头部添加一个元素
public void addFirst(E e) {
// 调用 linkFirst 方法将元素添加到链表的头部
linkFirst(e);
}
// 将元素添加到链表的头部
private void linkFirst(E e) {
// 获取当前的头节点
final Node<E> f = first;
// 创建一个新节点,新节点的前一个节点为 null,元素为 e,下一个节点为当前头节点
final Node<E> newNode = new Node<>(null, e, f);
// 将新节点设置为头节点
first = newNode;
if (f == null)
// 如果当前头节点为 null,说明链表为空,将新节点设置为尾节点
last = newNode;
else
// 否则,将当前头节点的前一个节点指向新节点
f.prev = newNode;
// 链表的大小加 1
size++;
// 记录链表结构修改的次数,用于迭代器的快速失败机制
modCount++;
}
在 addFirst 方法中,调用 linkFirst 方法将元素添加到链表的头部。在 linkFirst 方法中:
- 获取当前的头节点。
- 创建一个新节点,新节点的前一个节点为
null,元素为e,下一个节点为当前头节点。 - 将新节点设置为头节点。
- 如果当前头节点为
null,说明链表为空,将新节点设置为尾节点;否则,将当前头节点的前一个节点指向新节点。 - 链表的大小加 1。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
4.1.4 addLast(E e) 方法
addLast(E e) 方法用于在链表的尾部添加一个元素,与 add(E e) 方法功能相同。源码及注释如下:
// 在链表的尾部添加一个元素
public void addLast(E e) {
// 调用 linkLast 方法将元素添加到链表的尾部
linkLast(e);
}
在 addLast 方法中,直接调用 linkLast 方法将元素添加到链表的尾部,具体实现与 add(E e) 方法中的 linkLast 方法相同。
4.2 删除操作
4.2.1 remove() 方法
remove() 方法用于移除并返回链表的头部元素。源码及注释如下:
// 移除并返回链表的头部元素
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;
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 方法中:
- 获取当前的头节点。
- 如果当前头节点为
null,说明链表为空,抛出NoSuchElementException异常。 - 调用
unlinkFirst方法移除头节点。
在 unlinkFirst 方法中:
- 获取头节点的元素。
- 获取头节点的下一个节点。
- 将头节点的元素和下一个节点置为
null,以便垃圾回收。 - 将头节点的下一个节点设置为新的头节点。
- 如果头节点的下一个节点为
null,说明链表只有一个节点,将尾节点也置为null;否则,将新的头节点的前一个节点置为null。 - 链表的大小减 1。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
- 返回移除的头节点的元素。
4.2.2 remove(int index) 方法
remove(int index) 方法用于移除并返回指定索引位置的元素。源码及注释如下:
// 移除并返回指定索引位置的元素
public E remove(int index) {
// 检查索引是否越界
checkElementIndex(index);
// 调用 node 方法获取指定索引位置的节点
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) {
// 如果指定节点的前一个节点为 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) 方法中:
- 首先检查索引是否越界。
- 调用
node方法获取指定索引位置的节点。 - 调用
unlink方法移除指定节点。
在 unlink 方法中:
- 获取指定节点的元素、下一个节点和前一个节点。
- 如果指定节点的前一个节点为
null,说明指定节点是头节点,将指定节点的下一个节点设置为新的头节点;否则,将指定节点的前一个节点的下一个节点指向指定节点的下一个节点,并将指定节点的前一个节点置为null。 - 如果指定节点的下一个节点为
null,说明指定节点是尾节点,将指定节点的前一个节点设置为新的尾节点;否则,将指定节点的下一个节点的前一个节点指向指定节点的前一个节点,并将指定节点的下一个节点置为null。 - 将指定节点的元素置为
null,以便垃圾回收。 - 链表的大小减 1。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
- 返回移除的节点的元素。
4.2.3 remove(Object o) 方法
remove(Object o) 方法用于移除链表中第一个出现的指定元素。源码及注释如下:
// 移除链表中第一个出现的指定元素
public boolean remove(Object o) {
if (o == null) {
// 如果指定元素为 null,遍历链表,找到第一个值为 null 的节点并移除
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == 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) 方法中:
- 如果指定元素为
null,遍历链表,找到第一个值为null的节点,调用unlink方法移除该节点,并返回true。 - 如果指定元素不为
null,遍历链表,找到第一个值等于指定元素的节点,调用unlink方法移除该节点,并返回true。 - 如果遍历完整个链表都没有找到指定元素,返回
false。
4.2.4 removeFirstOccurrence(Object o) 方法
removeFirstOccurrence(Object o) 方法与 remove(Object o) 方法功能相同,用于移除链表中第一个出现的指定元素。源码及注释如下:
// 移除链表中第一个出现的指定元素
public boolean removeFirstOccurrence(Object o) {
// 调用 remove 方法移除指定元素
return remove(o);
}
在 removeFirstOccurrence 方法中,直接调用 remove 方法移除指定元素。
4.2.5 removeLastOccurrence(Object o) 方法
removeLastOccurrence(Object o) 方法用于移除链表中最后一个出现的指定元素。源码及注释如下:
// 移除链表中最后一个出现的指定元素
public boolean removeLastOccurrence(Object o) {
if (o == null) {
// 如果指定元素为 null,从尾部开始遍历链表,找到最后一个值为 null 的节点并移除
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
// 调用 unlink 方法移除该节点
unlink(x);
return true;
}
}
} else {
// 如果指定元素不为 null,从尾部开始遍历链表,找到最后一个值等于指定元素的节点并移除
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
// 调用 unlink 方法移除该节点
unlink(x);
return true;
}
}
}
return false;
}
在 removeLastOccurrence 方法中:
- 如果指定元素为
null,从尾部开始遍历链表,找到最后一个值为null的节点,调用unlink方法移除该节点,并返回true。 - 如果指定元素不为
null,从尾部开始遍历链表,找到最后一个值等于指定元素的节点,调用unlink方法移除该节点,并返回true。 - 如果遍历完整个链表都没有找到指定元素,返回
false。
4.3 查看操作
4.3.1 get(int index) 方法
get(int index) 方法用于返回指定索引位置的元素。源码及注释如下:
// 返回指定索引位置的元素
public E get(int index) {
// 检查索引是否越界
checkElementIndex(index);
// 调用 node 方法获取指定索引位置的节点,并返回该节点的元素
return node(index).item;
}
在 get(int index) 方法中:
- 首先检查索引是否越界。
- 调用
node方法获取指定索引位置的节点,并返回该节点的元素。
4.3.2 getFirst() 方法
getFirst() 方法用于返回链表的头部元素。源码及注释如下:
// 返回链表的头部元素
public E getFirst() {
// 获取当前的头节点
final Node<E> f = first;
if (f == null)
// 如果当前头节点为 null,说明链表为空,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
// 返回头节点的元素
return f.item;
}
在 getFirst 方法中:
- 获取当前的头节点。
- 如果当前头节点为
null,说明链表为空,抛出NoSuchElementException异常。 - 返回头节点的元素。
4.3.3 getLast() 方法
getLast() 方法用于返回链表的尾部元素。源码及注释如下:
// 返回链表的尾部元素
public E getLast() {
// 获取当前的尾节点
final Node<E> l = last;
if (l == null)
// 如果当前尾节点为 null,说明链表为空,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
// 返回尾节点的元素
return l.item;
}
在 getLast 方法中:
- 获取当前的尾节点。
- 如果当前尾节点为
null,说明链表为空,抛出NoSuchElementException异常。 - 返回尾节点的元素。
4.4 其他操作
4.4.1 size() 方法
size() 方法用于返回链表中元素的数量。源码及注释如下:
// 返回链表中元素的数量
public int size() {
// 直接返回 size 属性的值
return size;
}
在 size 方法中,直接返回 size 属性的值,该值表示链表中元素的数量。
4.4.2 isEmpty() 方法
isEmpty() 方法用于检查链表是否为空。源码及注释如下:
// 检查链表是否为空
public boolean isEmpty() {
// 判断 size 属性的值是否为 0,如果为 0 则说明链表为空,返回 true;否则返回 false
return size == 0;
}
在 isEmpty 方法中,判断 size 属性的值是否为 0,如果为 0 则说明链表为空,返回 true;否则返回 false。
4.4.3 contains(Object o) 方法
contains(Object o) 方法用于检查链表中是否包含指定的元素。源码及注释如下:
// 检查链表中是否包含指定的元素
public boolean contains(Object o) {
// 调用 indexOf 方法查找指定元素的索引,如果索引不为 -1 则说明链表中包含该元素,返回 true;否则返回 false
return indexOf(o) != -1;
}
// 返回指定元素在链表中第一次出现的索引,如果未找到则返回 -1
public int indexOf(Object o) {
int index = 0;
if (o == null) {
// 如果指定元素为 null,遍历链表,找到第一个值为 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 方法中,调用 indexOf 方法查找指定元素的索引,如果索引不为 -1 则说明链表中包含该元素,返回 true;否则返回 false。
在 indexOf 方法中:
- 如果指定元素为
null,遍历链表,找到第一个值为null的节点,返回其索引。 - 如果指定元素不为
null,遍历链表,找到第一个值等于指定元素的节点,返回其索引。 - 如果遍历完整个链表都没有找到指定元素,返回 -1。
4.4.4 clear() 方法
clear() 方法用于清空链表中的所有元素。源码及注释如下:
// 清空链表中的所有元素
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 方法中:
- 遍历链表,将每个节点的元素、前一个节点和下一个节点都置为
null,以便垃圾回收。 - 将头节点和尾节点都置为
null。 - 将链表的大小置为 0。
- 记录链表结构修改的次数,用于迭代器的快速失败机制。
五、核心方法的源码分析
5.1 迭代器相关方法
5.1.1 iterator() 方法
iterator() 方法用于返回一个迭代器,用于遍历链表中的元素。源码及注释如下:
// 返回一个迭代器,用于遍历链表中的元素
public Iterator<E> iterator() {
// 调用 listIterator 方法,从索引 0 开始获取迭代器
return listIterator(0);
}
// 返回一个列表迭代器,用于遍历链表中的元素
public ListIterator<E> listIterator(int index) {
// 检查索引是否越界
checkPositionIndex(index);
// 返回一个 ListItr 实例,该实例实现了 ListIterator 接口
return new ListItr(index);
}
// 列表迭代器类,实现了 ListIterator 接口
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);
// 如果索引等于链表的大小,下一个要返回的节点为 null;否则,调用 node 方法获取指定索引位置的节点
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;
// 下一个要返回的节点的索引加 1
nextIndex++;
// 返回上一次返回的节点的元素
return lastReturned.item;
}
// 判断是否还有上一个元素
public boolean hasPrevious() {
// 判断下一个要返回的节点的索引是否大于 0
return nextIndex > 0;
}
// 返回上一个元素
public E previous() {
// 检查链表结构是否被修改
checkForComodification();
if (!hasPrevious())
// 如果没有上一个元素,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
// 如果下一个要返回的节点为 null,将上一次返回的节点设置为尾节点;否则,将上一次返回的节点设置为下一个要返回的节点的前一个节点
lastReturned = next = (next == null)? last : next.prev;
// 下一个要返回的节点的索引减 1
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)
// 如果下一个要返回的节点等于上一次返回的节点
好的,以下继续对 LinkedList 进行源码层面的深入分析。
// 将下一个要返回的节点设置为上一次返回的节点的下一个节点
next = lastNext;
else
// 下一个要返回的节点的索引减 1
nextIndex--;
// 将上一次返回的节点置为 null
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();
// 将上一次返回的节点置为 null
lastReturned = null;
if (next == null)
// 如果下一个要返回的节点为 null,调用 linkLast 方法将元素添加到链表的尾部
linkLast(e);
else
// 否则,调用 linkBefore 方法在当前节点之前插入元素
linkBefore(e, next);
// 下一个要返回的节点的索引加 1
nextIndex++;
// 更新预期的修改次数
expectedModCount++;
}
// 检查链表结构是否被修改
final void checkForComodification() {
if (modCount != expectedModCount)
// 如果链表结构被修改,抛出 ConcurrentModificationException 异常
throw new ConcurrentModificationException();
}
}
在 ListItr 类中:
hasNext()方法:通过比较nextIndex和size的大小,判断是否还有下一个元素。next()方法:首先检查链表结构是否被修改,如果没有修改且有下一个元素,则更新lastReturned、next和nextIndex,并返回lastReturned的元素。hasPrevious()方法:通过判断nextIndex是否大于 0,来确定是否还有上一个元素。previous()方法:同样先检查链表结构,若有上一个元素,更新lastReturned、next和nextIndex,并返回lastReturned的元素。remove()方法:移除lastReturned节点,更新next和nextIndex,并更新预期的修改次数。set()方法:将lastReturned节点的元素设置为指定的值。add()方法:在当前位置插入一个元素,更新nextIndex和预期的修改次数。checkForComodification()方法:用于检查链表结构是否被修改,如果被修改则抛出ConcurrentModificationException异常,这是迭代器的快速失败机制。
5.1.2 descendingIterator() 方法
// 返回一个反向迭代器,用于从链表尾部开始遍历元素
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
// 反向迭代器类,实现了 Iterator 接口
private class DescendingIterator implements Iterator<E> {
// 获取正向的列表迭代器
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 类实现了反向迭代的功能,它利用 ListItr 类,将 hasNext() 映射到 ListItr 的 hasPrevious(),next() 映射到 ListItr 的 previous(),remove() 直接调用 ListItr 的 remove() 方法。
5.2 作为双端队列的方法
5.2.1 offer(E e) 方法
// 在队列尾部添加一个元素
public boolean offer(E e) {
// 调用 add 方法添加元素
return add(e);
}
offer(E e) 方法实际上调用了 add(E e) 方法,在链表的尾部添加元素。
5.2.2 offerFirst(E e) 方法
// 在队列头部添加一个元素
public boolean offerFirst(E e) {
// 调用 addFirst 方法添加元素
addFirst(e);
return true;
}
offerFirst(E e) 方法调用 addFirst(E e) 方法,在链表的头部添加元素。
5.2.3 offerLast(E e) 方法
// 在队列尾部添加一个元素
public boolean offerLast(E e) {
// 调用 addLast 方法添加元素
addLast(e);
return true;
}
offerLast(E e) 方法调用 addLast(E e) 方法,在链表的尾部添加元素。
5.2.4 peek() 方法
// 返回队列头部的元素,但不移除
public E peek() {
// 获取头节点
final Node<E> f = first;
// 如果头节点为 null,返回 null;否则返回头节点的元素
return (f == null)? null : f.item;
}
peek() 方法返回链表的头部元素,但不移除该元素,如果链表为空则返回 null。
5.2.5 peekFirst() 方法
// 返回队列头部的元素,但不移除
public E peekFirst() {
// 获取头节点
final Node<E> f = first;
// 如果头节点为 null,返回 null;否则返回头节点的元素
return (f == null)? null : f.item;
}
peekFirst() 方法与 peek() 方法功能相同,返回链表的头部元素,但不移除该元素,如果链表为空则返回 null。
5.2.6 peekLast() 方法
// 返回队列尾部的元素,但不移除
public E peekLast() {
// 获取尾节点
final Node<E> l = last;
// 如果尾节点为 null,返回 null;否则返回尾节点的元素
return (l == null)? null : l.item;
}
peekLast() 方法返回链表的尾部元素,但不移除该元素,如果链表为空则返回 null。
5.2.7 poll() 方法
// 移除并返回队列头部的元素
public E poll() {
// 获取头节点
final Node<E> f = first;
// 如果头节点为 null,返回 null;否则调用 unlinkFirst 方法移除并返回头节点的元素
return (f == null)? null : unlinkFirst(f);
}
poll() 方法移除并返回链表的头部元素,如果链表为空则返回 null。
5.2.8 pollFirst() 方法
// 移除并返回队列头部的元素
public E pollFirst() {
// 获取头节点
final Node<E> f = first;
// 如果头节点为 null,返回 null;否则调用 unlinkFirst 方法移除并返回头节点的元素
return (f == null)? null : unlinkFirst(f);
}
pollFirst() 方法与 poll() 方法功能相同,移除并返回链表的头部元素,如果链表为空则返回 null。
5.2.9 pollLast() 方法
// 移除并返回队列尾部的元素
public E pollLast() {
// 获取尾节点
final Node<E> l = last;
// 如果尾节点为 null,返回 null;否则调用 unlinkLast 方法移除并返回尾节点的元素
return (l == null)? null : unlinkLast(l);
}
// 移除尾节点
private E unlinkLast(Node<E> l) {
// 获取尾节点的元素
final E element = l.item;
// 获取尾节点的前一个节点
final Node<E> prev = l.prev;
// 将尾节点的元素和前一个节点置为 null,以便垃圾回收
l.item = null;
l.prev = null; // help GC
// 将尾节点的前一个节点设置为新的尾节点
last = prev;
if (prev == null)
// 如果尾节点的前一个节点为 null,说明链表只有一个节点,将头节点也置为 null
first = null;
else
// 否则,将新的尾节点的下一个节点置为 null
prev.next = null;
// 链表的大小减 1
size--;
// 记录链表结构修改的次数,用于迭代器的快速失败机制
modCount++;
return element;
}
pollLast() 方法移除并返回链表的尾部元素,如果链表为空则返回 null。unlinkLast 方法用于移除尾节点,更新链表的结构和大小。
5.2.10 push(E e) 方法
// 将元素压入栈顶(即链表头部)
public void push(E e) {
// 调用 addFirst 方法将元素添加到链表头部
addFirst(e);
}
push(E e) 方法将元素添加到链表的头部,实现了栈的压栈操作。
5.2.11 pop() 方法
// 从栈顶弹出元素(即移除并返回链表头部元素)
public E pop() {
// 调用 removeFirst 方法移除并返回链表头部元素
return removeFirst();
}
pop() 方法移除并返回链表的头部元素,实现了栈的弹栈操作。
5.3 克隆与序列化
5.3.1 clone() 方法
// 克隆当前 LinkedList
public Object clone() {
// 创建一个新的 LinkedList 实例
LinkedList<E> clone = superClone();
// 初始化克隆链表
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
// 遍历原链表,将元素添加到克隆链表中
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
// 调用父类的 clone 方法创建一个新的实例
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
clone() 方法首先调用 superClone() 方法创建一个新的 LinkedList 实例,然后将其 first、last、size 和 modCount 初始化,最后遍历原链表,将元素依次添加到克隆链表中。
5.3.2 序列化相关方法
// 序列化写入方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 写入非静态和非瞬态字段
s.defaultWriteObject();
// 写入链表的大小
s.writeInt(size);
// 遍历链表,写入每个元素
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
// 反序列化读取方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读取非静态和非瞬态字段
s.defaultReadObject();
// 读取链表的大小
int size = s.readInt();
// 依次读取元素并添加到链表中
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
writeObject() 方法用于将 LinkedList 对象序列化,先写入非静态和非瞬态字段,再写入链表的大小,最后遍历链表,将每个元素写入输出流。readObject() 方法用于反序列化,先读取非静态和非瞬态字段,再读取链表的大小,然后依次读取元素并添加到链表中。
六、性能分析
6.1 时间复杂度分析
- 插入操作:
- 在链表头部插入元素(如
addFirst、offerFirst、push)的时间复杂度为 ,因为只需要修改头节点的引用。 - 在链表尾部插入元素(如
add、addLast、offer、offerLast)的时间复杂度为 ,因为只需要修改尾节点的引用。 - 在指定索引位置插入元素(
add(int index, E element))的时间复杂度为 ,因为需要先找到指定索引位置的节点,平均需要遍历链表的一半元素。
- 在链表头部插入元素(如
- 删除操作:
- 删除链表头部元素(如
removeFirst、poll、pollFirst、pop)的时间复杂度为 ,因为只需要修改头节点的引用。 - 删除链表尾部元素(如
removeLast、pollLast)的时间复杂度为 ,因为只需要修改尾节点的引用。 - 删除指定索引位置的元素(
remove(int index))的时间复杂度为 ,因为需要先找到指定索引位置的节点,平均需要遍历链表的一半元素。 - 删除指定元素(
remove(Object o))的时间复杂度为 ,因为需要遍历链表找到该元素。
- 删除链表头部元素(如
- 查找操作:
- 根据索引查找元素(
get(int index))的时间复杂度为 ,因为需要从头节点或尾节点开始遍历链表,直到找到指定索引位置的节点。 - 查找指定元素的索引(
indexOf(Object o))的时间复杂度为 ,因为需要遍历链表找到该元素。
- 根据索引查找元素(
- 迭代操作:
- 使用迭代器遍历链表的时间复杂度为 ,因为需要依次访问链表中的每个元素。
6.2 空间复杂度分析
LinkedList 的空间复杂度为 ,其中 是链表中元素的数量。每个节点需要额外的空间来存储指向前一个节点和后一个节点的引用,因此相对于基于数组的实现(如 ArrayList),LinkedList 需要更多的空间来存储相同数量的元素。
6.3 性能比较与适用场景
与 ArrayList 相比,LinkedList 在插入和删除操作上具有优势,尤其是在链表的头部和尾部进行操作时,时间复杂度为 。而 ArrayList 在随机访问操作上具有优势,时间复杂度为 。因此,LinkedList 适用于以下场景:
- 需要频繁进行插入和删除操作,尤其是在链表的头部和尾部进行操作。
- 不需要频繁进行随机访问操作。
而 ArrayList 适用于以下场景:
- 需要频繁进行随机访问操作。
- 插入和删除操作主要集中在列表的尾部。
七、使用示例
7.1 作为列表使用
import java.util.LinkedList;
import java.util.List;
public class LinkedListAsListExample {
public static void main(String[] args) {
// 创建一个 LinkedList 实例
List<String> list = new LinkedList<>();
// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// 打印列表元素
System.out.println("List elements: " + list);
// 获取指定索引位置的元素
String element = list.get(1);
System.out.println("Element at index 1: " + element);
// 移除指定索引位置的元素
list.remove(1);
System.out.println("List elements after removal: " + list);
}
}
在这个示例中,LinkedList 作为 List 接口的实现类使用,演示了添加元素、获取元素和移除元素的操作。
7.2 作为双端队列使用
import java.util.LinkedList;
import java.util.Deque;
public class LinkedListAsDequeExample {
public static void main(String[] args) {
// 创建一个 LinkedList 实例作为双端队列
Deque<String> deque = new LinkedList<>();
// 在队列头部添加元素
deque.offerFirst("Apple");
deque.offerFirst("Banana");
// 在队列尾部添加元素
deque.offerLast("Cherry");
// 打印队列元素
System.out.println("Deque elements: " + deque);
// 移除并返回队列头部元素
String firstElement = deque.pollFirst();
System.out.println("Removed first element: " + firstElement);
// 移除并返回队列尾部元素
String lastElement = deque.pollLast();
System.out.println("Removed last element: " + lastElement);
// 打印队列元素
System.out.println("Deque elements after removal: " + deque);
}
}
在这个示例中,LinkedList 作为 Deque 接口的实现类使用,演示了在队列头部和尾部添加元素、移除元素的操作。
7.3 作为栈使用
import java.util.LinkedList;
import java.util.Stack;
public class LinkedListAsStackExample {
public static void main(String[] args) {
// 创建一个 LinkedList 实例作为栈
Stack<String> stack = new LinkedList<>();
// 压栈操作
stack.push("Apple");
stack.push("Banana");
stack.push("Cherry");
// 打印栈元素
System.out.println("Stack elements: " + stack);
// 弹栈操作
String topElement = stack.pop();
System.out.println("Popped element: " + topElement);
// 打印栈元素
System.out.println("Stack elements after pop: " + stack);
}
}
在这个示例中,LinkedList 作为 Stack 使用,演示了压栈和弹栈的操作。
八、总结与展望
8.1 总结
LinkedList 是 Java 集合框架中一个非常重要的数据结构,它基于双向链表实现,具有以下特点:
- 灵活的插入和删除操作:在链表的头部和尾部进行插入和删除操作的时间复杂度为 ,在中间位置进行操作的时间复杂度为 ,因此适用于需要频繁进行插入和删除操作的场景。
- 不支持随机访问:
LinkedList不支持随机访问,需要从头节点或尾节点开始遍历链表,直到找到目标元素,时间复杂度为 ,因此在需要频繁进行随机访问操作的场景中性能不如ArrayList。 - 实现了多个接口:
LinkedList实现了List和Deque接口,既可以作为普通的列表使用,又可以作为双端队列使用,支持在队列的两端进行元素的插入和删除操作,还可以作为栈使用,支持压栈和弹栈操作。 - 支持克隆和序列化:
LinkedList支持对象的克隆和序列化操作,可以方便地进行对象的复制和持久化存储。
8.2 展望
随着 Java 技术的不断发展,LinkedList 可能会在以下方面得到进一步的优化和改进:
- 性能优化:尽管
LinkedList在插入和删除操作上具有一定的优势,但在某些场景下,其性能仍然可以进一步提升。例如,可以通过优化节点的内存布局、减少引用的开销等方式来提高LinkedList的性能。 - 并发支持:在多线程环境下,
LinkedList不是线程安全的,需要使用额外的同步机制来保证线程安全。未来可能会提供更高效的并发实现,以满足多线程环境下的使用需求。 - 与其他数据结构的融合:可以将
LinkedList与其他数据结构进行融合,以发挥各自的优势,实现更复杂的功能。例如,可以将LinkedList与哈希表结合,实现一个有序的哈希表。
总之,LinkedList 作为 Java 集合框架中的一个重要组成部分,在实际开发中具有广泛的应用场景。通过深入理解其使用原理和性能特点,开发者可以更好地选择合适的数据结构,优化代码性能,提高开发效率。
以上文章详细分析了 Java LinkedList 的使用原理,从源码层面深入剖析了其内部结构、核心方法的实现以及性能特点,并通过示例代码展示了其使用方式。希望这篇文章能够帮助你更好地理解和使用 LinkedList。