深度揭秘:Java LinkedList 源码级使用原理剖析

深度揭秘: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 方法的步骤如下:
    1. 获取当前的尾节点 l。
    2. 创建一个新的节点 newNode,其前一个节点为 l,元素为 e,下一个节点为 null。
    3. 将尾节点更新为 newNode。
    4. 如果 l 为 null,说明链表为空,将头节点也更新为 newNode。
    5. 否则,将原尾节点的 next 引用指向 newNode。
    6. 链表元素数量 size 加 1。
    7. 增加 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) 方法的步骤如下:
    1. 调用 checkPositionIndex 方法检查索引是否越界。
    2. 如果索引等于链表的大小,调用 linkLast 方法在尾部添加元素。
    3. 否则,调用 node(index) 方法获取指定索引位置的节点,然后调用 linkBefore 方法在该节点之前插入元素。
  • node(int index) 方法会根据索引的位置,决定是从链表头部还是尾部开始遍历,以提高查找效率。
  • linkBefore 方法的步骤如下:
    1. 获取指定节点的前一个节点 pred。
    2. 创建一个新的节点 newNode,其前一个节点为 pred,元素为 e,下一个节点为 succ。
    3. 将指定节点的 prev 引用指向 newNode。
    4. 如果 pred 为 null,说明指定节点是头节点,将头节点更新为 newNode。
    5. 否则,将 pred 的 next 引用指向 newNode。
    6. 链表元素数量 size 加 1。
    7. 增加 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) 方法的步骤如下:
    1. 调用 checkPositionIndex 方法检查索引是否越界。
    2. 将集合转换为数组 a。
    3. 如果数组长度为 0,返回 false。
    4. 根据索引位置确定前驱节点 pred 和后继节点 succ。
    5. 遍历数组 a,依次创建新节点并插入到链表中。
    6. 如果 succ 为 null,说明是在尾部添加元素,将尾节点更新为最后一个新节点。
    7. 否则,将最后一个新节点的 next 引用指向 succ,succ 的 prev 引用指向最后一个新节点。
    8. 更新链表元素数量 size。
    9. 增加 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 方法的步骤如下:
    1. 获取当前的头节点 f。
    2. 如果 f 为 null,说明链表为空,抛出 NoSuchElementException 异常。
    3. 调用 unlinkFirst 方法删除头节点。
  • unlinkFirst 方法的步骤如下:
    1. 获取头节点的元素 element。
    2. 获取头节点的下一个节点 next。
    3. 将头节点的元素和下一个节点引用置为 null,帮助垃圾回收。
    4. 将头节点更新为 next。
    5. 如果 next 为 null,说明链表只有一个节点,将尾节点也置为 null。
    6. 否则,将 next 的 prev 引用置为 null。
    7. 链表元素数量 size 减 1。
    8. 增加 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) 方法的步骤如下:
    1. 调用 checkElementIndex 方法检查索引是否越界。
    2. 调用 node(index) 方法获取指定索引位置的节点。
    3. 调用 unlink 方法删除该节点。
  • unlink 方法的步骤如下:
    1. 获取指定节点的元素 element。
    2. 获取指定节点的下一个节点 next 和前一个节点 prev。
    3. 如果 prev 为 null,说明指定节点是头节点,将头节点更新为 next。
    4. 否则,将 prev 的 next 引用指向 next,并将指定节点的 prev 引用置为 null。
    5. 如果 next 为 null,说明指定节点是尾节点,将尾节点更新为 prev。
    6. 否则,将 next 的 prev 引用指向 prev,并将指定节点的 next 引用置为 null。
    7. 将指定节点的元素置为 null。
    8. 链表元素数量 size 减 1。
    9. 增加 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) 方法的步骤如下:
    1. 如果要删除的元素 o 为 null,遍历链表,找到第一个元素为 null 的节点,调用 unlink 方法删除该节点。
    2. 如果要删除的元素 o 不为 null,遍历链表,找到第一个匹配的节点,调用 unlink 方法删除该节点。
    3. 如果找到并删除了元素,返回 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) 方法的步骤如下:
    1. 调用 checkElementIndex 方法检查索引是否越界。
    2. 调用 node(index) 方法获取指定索引位置的节点。
    3. 返回该节点的元素。
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() 方法的步骤如下:
    1. 获取当前的头节点 f。
    2. 如果 f 为 null,说明链表为空,抛出 NoSuchElementException 异常。
    3. 返回 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() 方法的步骤如下:
    1. 获取当前的尾节点 l。
    2. 如果 l 为 null,说明链表为空,抛出 NoSuchElementException 异常。
    3. 返回 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) 方法的步骤如下:
    1. 调用 checkElementIndex 方法检查索引是否越界。
    2. 调用 node(index) 方法获取指定索引位置的节点 x。
    3. 获取 x 原来的元素 oldVal。
    4. 将 x 的元素更新为 element。
    5. 返回 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() 方法的步骤如下:
    1. 获取当前的头节点 f。
    2. 如果 f 为 null,说明队列为空,返回 null。
    3. 否则,调用 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 变量的值,时间复杂度为 O ( 1 ) O(1) 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 维护了尾节点引用,直接在尾部插入新节点,时间复杂度为 O ( 1 ) O(1) O(1) 。
    • 在指定位置插入元素(add(int index, E element) ):需要先找到指定位置的节点,平均需要遍历链表一半的节点(node(index) 方法),时间复杂度为 O ( n ) O(n) O(n) ,其中 n n n 为链表长度;找到节点后插入操作本身时间复杂度为 O ( 1 ) O(1) O(1) ,因此整体插入操作时间复杂度为 O ( n ) O(n) O(n) 。
  • 删除操作 :
    • 删除头部元素(removeFirst() ):直接操作头节点,时间复杂度为 O ( 1 ) O(1) O(1) 。
    • 删除指定位置元素(remove(int index) ):同样需要先找到指定位置的节点,时间复杂度为 O ( n ) O(n) O(n) ,找到后删除操作时间复杂度为 O ( 1 ) O(1) O(1) ,整体删除操作时间复杂度为 O ( n ) O(n) O(n) 。
    • 删除指定元素(remove(Object o) ):需要遍历链表查找元素,最坏情况下遍历整个链表,时间复杂度为 O ( n ) O(n) O(n) 。
  • 查找操作 :
    • 获取指定位置元素(get(int index) ):查找指定位置节点时间复杂度为 O ( n ) O(n) O(n) ,获取元素本身时间复杂度为 O ( 1 ) O(1) O(1) ,整体时间复杂度为 O ( n ) O(n) O(n) 。
    • 判断元素是否存在(contains(Object o) ):需要遍历链表查找元素,时间复杂度为 O ( n ) O(n) O(n) 。

8.2 空间复杂度分析

LinkedList 每个节点存储元素以及前后节点引用,空间占用与元素数量成正比 。其空间复杂度为 O ( n ) O(n) O(n) ,其中 n n n 为链表中元素的数量 。与基于数组实现的 ArrayList 相比,LinkedList 不需要预先分配固定大小的内存空间,但每个节点额外的引用会占用一定内存 。

8.3 与 ArrayList 的性能对比

  • 随机访问 :ArrayList 基于数组实现,通过索引直接访问元素,时间复杂度为 O ( 1 ) O(1) O(1) ;LinkedList 需要遍历链表,时间复杂度为 O ( n ) O(n) O(n) ,因此在随机访问场景下 ArrayList 性能更优 。
  • 插入和删除 :在链表中间或头部插入、删除元素时,LinkedList 只需修改节点引用,时间复杂度为 O ( 1 ) O(1) O(1) (不考虑查找节点的时间);而 ArrayList 可能需要移动大量元素,时间复杂度为 O ( n ) O(n) O(n) 。但在数组尾部插入元素时,ArrayList 平均时间复杂度接近 O ( 1 ) O(1) O(1) (摊还分析) 。因此,频繁在中间或头部进行插入、删除操作时,LinkedList 性能更好;而在尾部插入以及随机访问较多的场景下,ArrayList 更具优势 。

九、总结与展望

9.1 总结

通过深入分析 Java LinkedList 的源码,我们全面了解了其使用原理。LinkedList 采用双向链表结构,通过 Node 节点类存储元素和前后节点引用,实现了高效的插入和删除操作 。它不仅作为普通列表使用,还通过实现 Deque 接口,支持队列和栈的操作 。在方法实现上,添加、删除、获取等操作都围绕链表节点的引用修改和遍历展开 。同时,其迭代器的实现保证了遍历过程中的并发安全检查 。

在性能方面,LinkedList 在插入和删除操作(尤其是在链表头部或中间)上具有优势,但随机访问性能较差 。这使得它适用于需要频繁插入、删除元素,而随机访问需求较少的场景,如实现队列、栈数据结构,或者处理数据频繁变动的链表数据 。

9.2 展望

随着 Java 技术的不断发展,未来 LinkedList 可能会在性能优化和功能扩展上有所改进 。例如,在查找性能优化方面,可能会引入更高效的查找算法或数据结构辅助,减少遍历操作带来的性能损耗 。在并发处理上,或许会进一步增强对多线程环境的支持,提供更高效、安全的并发操作方式 。此外,结合新的 Java 特性,如 Java 8 引入的流操作、Java 11 的 HTTP 客户端等,LinkedList 可能会在功能上进行拓展,更好地适应复杂多变的开发需求 。

对于开发者而言,深入理解 LinkedList 的原理和特性,有助于在实际开发中根据具体场景选择合适的数据结构,编写出更高效、健壮的代码 。

相关推荐
2501_933670791 小时前
2027校招销售运营面试:大数据管理与应用专业如何拆解漏斗分析
大数据·人工智能·面试
丑陋小蚊子8 小时前
邮件投简历,正文和附件各写什么?
面试·求职招聘·大学生·简历·简历下载
架构技术专栏9 小时前
学习率:一次参数更新,到底应该改多少
面试
架构技术专栏9 小时前
AI Agent 框架怎么选:从一次制度查询拆出技术边界
后端·面试
传奇开心果编程11 小时前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
三少爷的鞋11 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
花间相见13 小时前
【后端开发|Redis进阶01】—— Redis分布式锁全解:从SETNX到Redisson看门狗,再到RedLock
后端·面试
Dovis(誓平步青云)15 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
ShineWinsu17 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
晚风叙码17 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb