集合-List-LinkedList

9 阅读27分钟

概述
LinkedList 是 Java 集合框架中一个“身份双重”的经典实现:它既是一个基于双向链表、支持按索引访问的 List,也是一个无界双端队列 Deque。正因为底层采用 Node 节点加 first/last 指针的存储模型,LinkedList 在头尾插入删除场景展现出 O(1) 的极致性能,但同时也因内存离散分布、无法随机寻址而付出了遍历成本。本文以 JDK 8 源码为锚点,系统拆解其存储结构、核心操作指针变化、迭代器 fail-fast 机制、序列化设计以及与 ArrayList 的全方位对比,并针对并发陷阱给出安全的替代方案。阅读完本文,你将获得从源码细节到生产级选型的完整认知链条。

  • 双向链表的存储结构与内存特征:每个节点持有前后指针,内存离散分布,无扩容开销但节点对象头占用较大。
  • 头尾插入/删除的 O(1) 实现机制:通过 first/last 指针和 linkFirst/linkLast/unlinkFirst/unlinkLast 方法实现常量时间操作。
  • 随机访问的二分遍历优化get(index) 根据 indexsize/2 的关系决定从头或尾遍历,将平均时间复杂度控制在 O(n/2)。
  • 与 ArrayList 的全维度对比:从数据结构、时间复杂度、内存占用、CPU缓存局部性、使用场景等角度进行深度对比。
  • fail-fast 迭代器在链表中的落地ListItr 通过 modCount 校验实现 fail-fast,支持双向遍历和 remove/set/add 操作。
  • 非线程安全的原因及替代方案:并发修改链表指针可导致数据损坏,对比三种线程安全 List 方案,推荐并发队列替代品。

全文组织架构说明

  graph TD
    Start["LinkedList 深度解析"] --> P1
    Start --> P2
    Start --> P3
    Start --> P4
    Start --> P5
    Start --> P6
    subgraph P1["Part 1: 基础认知篇"]
        M1["模块1: 定义与特性"]
        M2["模块2: 接口继承体系"]
    end
    subgraph P2["Part 2: 存储与构造篇"]
        M3["模块3: 存储结构(Node)"]
        M4["模块4: 构造方法与初始化"]
    end
    subgraph P3["Part 3: 核心原理篇"]
        M5["模块5: 头尾插入"]
        M6["模块6: 指定位置插入"]
        M7["模块7: 删除操作"]
        M8["模块8: 查询操作"]
    end
    subgraph P4["Part 4: 双端队列与迭代篇"]
        M9["模块9: Deque实现"]
        M10["模块10: 迭代器剖析"]
        M11["模块11: 序列化设计"]
    end
    subgraph P5["Part 5: 对比与并发篇"]
        M12["模块12: ArrayList对比"]
        M13["模块13: 并发问题与方案"]
    end
    subgraph P6["Part 6: 总结与面试篇"]
        M14["模块14: 最佳实践"]
        M15["模块15: 性能总结"]
        M16["模块16: 面试高频专题"]
    end

上图将全文划分为六大篇章,共 16 个模块,由基础认知逐步深入到源码实现、并发对策,最终汇总性能总结与面试专题。

  • Part 1:基础认知篇 — 奠定概念基石:定义 LinkedList 的核心特性与适用场景,并梳理其接口与继承体系。
  • Part 2:存储与构造篇 — 深入存储层:分析 Node 内部类、first/last 字段以及构造器与批量添加的源码细节。
  • Part 3:核心原理篇 — 拆解核心操作:通过流程图逐行阐释头尾插入、指定位置插入、删除与查询的指针变化和二分优化。
  • Part 4:双端队列与迭代篇 — 拓展双端队列与迭代:说明 Deque 方法映射、迭代器 fail-fast 机制以及 transient 序列化设计。
  • Part 5:对比与并发篇 — 聚焦对比与并发:全方位对比 ArrayList,揭示线程不安全原因并给出三种安全替代方案及并发队列推荐。
  • Part 6:总结与面试篇 — 汇总生产实践与面试:梳理注意事项、性能速查表,并独立归纳 10 个高频面试题及追问、加分回答。

Part 1:基础认知篇

模块 1:定义、核心特性与适用场景

java.util.LinkedList 是基于双向链表实现的 ListDeque 接口集合类。它允许 null 元素,内部通过静态内部类 Node<E> 维护元素间的关联,并用 firstlast 变量直接持有头尾引用。

6 大核心特性:

  1. 双向链表结构:每个元素都是一个 Node 节点,同时拥有 prev(前驱)和 next(后继)指针。
  2. 列表与双端队列双接口:同时实现 ListDeque,既能按索引操作,也能当栈、队列或双端队列使用。
  3. 头尾操作极致高效addFirst / addLast / removeFirst / removeLast 时间复杂度均为 O(1)。
  4. 禁止随机访问get(int index) 必须逐节点遍历,复杂度 O(n),但通过二分方向优化了常数时间。
  5. 非线程安全:没有内置同步机制,并发读写会抛出 ConcurrentModificationException 甚至造成链表结构损坏。
  6. 支持克隆与序列化:实现 CloneableSerializable,但序列化时不会直接序列化 Node 链,而是逐元素写入,以此屏蔽内部结构。

适用与反例场景决策图:

graph TD
    START["开始: 需要列表/队列集合"] --> Q1{"是否主要为头尾增删?"}
    Q1 -->|"是"| Q2{"需要线程安全?"}
    Q2 -->|"是"| R1["ConcurrentLinkedDeque 或 LinkedBlockingDeque"]
    Q2 -->|"否"| R2["LinkedList"]
    Q1 -->|"否"| Q3{"大量依靠索引随机访问?"}
    Q3 -->|"是"| R3["ArrayList"]
    Q3 -->|"否"| Q4{"需要先进先出队列?"}
    Q4 -->|"是"| Q5{"元素是否可为null?"}
    Q5 -->|"是"| R4["LinkedList"]
    Q5 -->|"否"| R5["ArrayDeque (推荐)"]
    Q4 -->|"否"| R6["考虑其他 List 实现"]

图 1.1 — 适用场景决策树详细说明

步骤 1:判断是否主要为头尾增删操作

  • 若业务逻辑频繁调用 addFirst / addLast / removeFirst / removeLast(如实现栈、FIFO 队列),则 LinkedList 的头尾 O(1) 特性独树一帜。
  • 若否,则进入随机访问评估。

步骤 2:线程安全需求(头尾场景)

  • 如果需要线程安全,JDK 提供了 ConcurrentLinkedDequeLinkedBlockingDeque,它们基于 CAS 或锁,避免同步整个链表。
  • 如果不需要,LinkedList 即为最佳原生选择。

步骤 3:大量依靠索引随机访问?

  • get(index) 需要折半遍历,当集合较大且索引访问占比高时,性能远不及基于数组的 ArrayList,此时应直接选择 ArrayList。
  • 若索引访问不主要,再检查是否需要 FIFO 队列。

步骤 4:元素可否为 null

  • LinkedList 允许 null 元素,因此如果业务数据中 null 有业务含义,可使用 LinkedList 做队列。
  • 若不允许 null,则 ArrayDeque 是更高效的选择,因为它没有节点开销,空间利用率更好。

核心决策点(关键点提炼)

  • 头尾操作多 → LinkedList。
  • 随机访问多 → ArrayList。
  • 并发 + 头尾操作多 → ConcurrentLinkedDeque / LinkedBlockingDeque。
  • 纯队列且无 null → ArrayDeque。

模块 2:接口与继承体系

LinkedList 的继承体系展现了“既是 List,也是 Deque”的设计:

classDiagram
    class Iterable {
        <<interface>>
    }
    class Collection {
        <<interface>>
    }
    class List {
        <<interface>>
    }
    class Deque {
        <<interface>>
    }
    class Queue {
        <<interface>>
    }
    class AbstractCollection {
        <<abstract>>
    }
    class AbstractList {
        <<abstract>>
    }
    class AbstractSequentialList {
        <<abstract>>
    }
    class LinkedList {
        -Node first
        -Node last
        -int size
        +addFirst(E e)
        +addLast(E e)
        +get(int index)
        +remove(Object o)
        +push(E e)
        +pop()
    }
    class Cloneable {
        <<interface>>
    }
    class Serializable {
        <<interface>>
    }
    Iterable <|-- Collection
    Collection <|-- List
    Collection <|-- Queue
    Queue <|-- Deque
    AbstractCollection <|-- AbstractList
    AbstractList <|-- AbstractSequentialList
    AbstractSequentialList <|-- LinkedList
    List <|-- LinkedList
    Deque <|-- LinkedList
    Cloneable <|-- LinkedList
    Serializable <|-- LinkedList

图 2.1 — 接口与继承体系类图说明

分层解读:

  • 顶层接口Iterable 赋予集合 for-each 遍历能力。
  • Collection → List / Queue / DequeList 提供基于索引的顺序操作,Queue 提供单向队列方法,Deque 则扩展为双端队列(可在两端插入、移除和检查元素)。
  • 抽象骨架类AbstractCollectionAbstractListAbstractSequentialList。其中 AbstractSequentialList 最小化了实现 List 所需的工作量:它只要求子类提供 listIterator()size(),而 getsetaddremove 等全部依赖迭代器。
  • LinkedList 实现:直接继承 AbstractSequentialList,同时实现 ListDequeCloneableSerializable。这使得 LinkedList 既能按索引遍历,又具备双端队列的全部操作。

关键点提炼:

  • AbstractSequentialList 是顺序访问 List 的骨架,区别于 AbstractList 的随机访问模型。
  • 实现 Deque 使得 LinkedList 具备了超出 List 的能力(栈、队列、双端操作)。

Part 2:存储与构造篇

模块 3:存储结构与核心字段(源码剖析)

LinkedList 的真实存储单元是内部类 Node<E>

private static class Node<E> {
    E item;
    Node<E> prev;
    Node<E> next;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.prev = prev;
        this.next = next;
    }
}

LinkedList 自身只保留三个字段:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

结构示意类图:

classDiagram
    class LinkedList {
        -Node~E~ first
        -Node~E~ last
        -int size
    }
    class Node~E~ {
        -E item
        -Node~E~ prev
        -Node~E~ next
    }
    LinkedList "1" *-- "0..1" Node : first
    LinkedList "1" *-- "0..1" Node : last
    Node "1" --> "0..1" Node : prev
    Node "1" --> "0..1" Node : next
    Node "1" --> "0..1" Node : prev
    Node "1" --> "0..1" Node : next

图 3.1 存储结构类图说明

步骤解读:

  1. LinkedList 实例持有 firstlast 引用,若链表为空则为 null
  2. Node 实例之间通过 prevnext 形成双向链路:头节点 prev == null尾节点 next == null
  3. 非空链表遍历可从头 first 向后,或从 last 向前。
  4. size 记录节点个数,用于边界检查和二分优化。

边界条件分析:

  • 空链表first == null && last == nullsize == 0
  • 单元素链表first == last,且 first.prev == null && first.next == null
  • size 未使用 volatile,表明非线程安全。

模块 4:构造方法与初始化

LinkedList 提供两个构造器,核心在于如何将外部集合转为链表:

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

真正的逻辑在 addAll

public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    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 += numNew;
    modCount++;
    return true;
}

Demo 代码 4.1 — 构造与批量添加展示:

import java.util.LinkedList;
import java.util.Arrays;

public class LinkedListConstructionDemo {
    public static void main(String[] args) {
        // 无参构造
        LinkedList<String> list1 = new LinkedList<>();
        list1.add("A");

        // 通过集合构造
        LinkedList<String> list2 = new LinkedList<>(Arrays.asList("B", "C", "D"));
        System.out.println(list2); // [B, C, D]

        // 批量添加到指定位置
        list2.addAll(1, Arrays.asList("X", "Y"));
        System.out.println(list2); // [B, X, Y, C, D]
    }
}

源码解读要点:

  • toArray() 先获取集合快照,避免混合遍历修改。
  • node(index) 定位插入点(succ),并将 pred 置为其前驱。若 index == sizesucc = nullpred = last,直接在尾部追加。
  • 循环创建节点并将 pred 不断后移,最后通过 pred.next = succ 与后续链表连接。
  • modCount++ 记录一次结构性修改,供迭代器检测。

Part 3:核心原理篇

模块 5:头尾插入(addFirst / addLast)源码深度剖析

addFirstaddLast 分别调用 linkFirstlinkLast,它们是最高频的 O(1) 操作。

linkFirst 源码(简化注释):

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

addFirst 指针变化流程图:

flowchart TD
    S[addFirst e] --> A["Node f = first"]
    A --> B["创建新节点 newNode<br/>prev=null, item=e, next=f"]
    B --> C["first = newNode"]
    C --> D{"f == null ?"}
    D -->|是: 空链表| E["last = newNode"]
    D -->|否: 非空链表| F["f.prev = newNode"]
    E --> G["size++ ; modCount++"]
    F --> G
    G --> END["结束"]

图 5.1 — addFirst 流程图层次分明说明

步骤 1:Node f = first
源码对应:final Node<E> f = first;
保存旧的头节点引用,用于后续判定空链表与新节点的链接。

步骤 2:创建 newNode
源码对应:final Node<E> newNode = new Node<>(null, e, f);
新节点的 prevnull(因为它将成为新头),next 指向旧的 first

步骤 3:first = newNode
first 字段更新为新节点,完成头部替换。无论链表是否为空,此步均执行。

步骤 4:if (f == null)

  • 空链表分支fnull,说明此时链表无元素,因此 last 也必须指向同一个新节点 newNode,使得链表仅有一个节点。
  • 非空分支f.prev = newNode,将旧头节点的前驱指针指向新节点,完成双向链接。

步骤 5:size++ ; modCount++
记录元素个数与结构修改次数。

边界条件分析:

  • 空链表firstlast 同时指向 newNodenewNode.prevnewNode.next 皆为 null
  • 单元素链表:执行 addFirst 后,新头节点的 next 指向原单节点,原单节点的 prev 指向新头。last 仍指向原单节点。
  • 多次 addFirst 仅改变 firstlast 自第一次插入后保持不变。

linkLast 是镜像操作,只需将上述逻辑中的 first 换为 lastnext / prev 方向对调。


模块 6:指定位置插入(add(int index, E element))源码剖析

add(index, element) 涉及结点定位与 linkBefore,时间复杂度 O(n)。

public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

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;
    }
}

linkBefore 实现:

void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

插入全流程流程图:

flowchart TD
    START["add(index, element)"] --> CHECK["checkPositionIndex(index)"]
    CHECK --> COMPARE{"index == size ?"}
    COMPARE -->|是| CALL_LAST["linkLast(element)"]
    COMPARE -->|否| LOCATE["node(index) 定位 succ"]
    LOCATE --> HALF{"index < size/2 ?"}
    HALF -->|是| FORWARD["从 first 向后遍历 index 步"]
    HALF -->|否| BACKWARD["从 last 向前遍历 (size-1-index) 步"]
    FORWARD --> LB["linkBefore(element, succ)"]
    BACKWARD --> LB
    LB --> PRED["pred = succ.prev"]
    PRED --> NEW["newNode(pred, e, succ)"]
    NEW --> SUC_PREV["succ.prev = newNode"]
    SUC_PREV --> PRED_NULL{"pred == null ?"}
    PRED_NULL -->|是| SET_FIRST["first = newNode"]
    PRED_NULL -->|否| SET_NEXT["pred.next = newNode"]
    SET_FIRST --> INC["size++, modCount++"]
    SET_NEXT --> INC
    CALL_LAST --> INC
    INC --> END["结束"]

图 6.1 — add(index, element) 流程图详解

分步骤解读:

1. 索引范围检查
checkPositionIndex(index) 确保 0 <= index <= size,否则抛出 IndexOutOfBoundsException

2. index == size 快速路径
index == size,等价于在尾部插入,直接走 linkLast,避免不必要的节点定位。

3. node(index) 二分决策

  • 核心优化:比较 indexsize >> 1(即 size/2),选择靠近目标的一端开始遍历。
  • 前向遍历Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; 循环结束条件为 i == index,返回第 index 个节点。
  • 后向遍历Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; 反向走直到 i == index

4. linkBefore 插入

  • pred = succ.prev:取得原位置节点的前驱。
  • 新节点构造newNode(pred, e, succ),其 prev 指向 prednext 指向 succ
  • succ.prev = newNode:将后置节点的前驱指针连接到新节点。
  • pred == null 分支:若 succ 原本为头节点(pred == null),则 first 需要更新为新节点;否则 pred.next = newNode

关键点提炼:

  • 二分方向使遍历步骤数最多为 size/2,但仍为 O(n)。
  • 插入指针操作与 linkFirst/linkLast 类似,核心是维护 predsucc 的相邻关系。

边界条件分析:

  • index == 0succ 为原 firstpred == null,因此 first 会指向新节点,新节点成为新头。
  • index == size:走 linkLast 分支,此时 last 指向新尾节点。
  • 单元素链表:在 index 0 插入前元素,转变为双元素;node(1) 返回尾节点,可正常插入为第二个元素。

模块 7:删除操作(removeFirst / removeLast / remove(Object))源码剖析

删除操作的精华在于 unlink 系列方法,它们将节点从链中摘除并置空引用以帮助 GC。

removeFirst 与 unlinkFirst:

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

remove(Object) 与通用 unlink:

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    final E element = x.item;
    final Node<E> prev = x.prev;
    final Node<E> next = x.next;

    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;
    size--;
    modCount++;
    return element;
}

unlink 节点摘除流程图:

flowchart TD
    S["unlink(x)"] --> A["element = x.item<br/>prev = x.prev<br/>next = x.next"]
    A --> B{"prev == null ?"}
    B -->|是 头节点| C["first = next"]
    B -->|否 非头节点| D["prev.next = next<br/>x.prev = null"]
    C --> E{"next == null ?"}
    D --> E
    E -->|是 尾节点| F["last = prev"]
    E -->|否 非尾节点| G["next.prev = prev<br/>x.next = null"]
    F --> H["x.item = null"]
    G --> H
    H --> I["size--, modCount++"]
    I --> END["返回 element"]

图 7.1 — unlink 流程图详细说明

步骤分解对应源码:

  1. 获取变量element = x.item; prev = x.prev; next = x.next;
    备份节点数据及前后指针,后续操作将基于这些引用修改链表结构。

  2. 处理前驱 (prev == null) 分支

    • 是头节点first = next,链表头直接后移,丢弃当前节点。
    • 非头节点prev.next = next,前驱的 next 跨过当前节点,直接指向当前节点的 next;随后 x.prev = null 断开前驱引用,帮助 GC。
  3. 处理后继 (next == null) 分支

    • 是尾节点last = prev,链尾前移。
    • 非尾节点next.prev = prev,后继的 prev 跨过当前节点指向 prev;同时 x.next = null 断开后继引用。
  4. 清理与维护
    x.item = null 释放元素引用;size-- 更新大小;modCount++ 记录结构修改。

关键点提炼:

  • 指针修改的顺序保证链表不出现断口:先链接前后节点,再断开当前节点的指针对 GC 友好。
  • 删除操作始终返回被删元素,供调用者使用。

边界条件分析:

  • 删除唯一节点prev == null && next == null,执行后 first = nulllast = null,链表回到空状态。
  • 删除头节点(但非唯一):prev == nullfirst 更新为原第二节点,next.prev 被置 null
  • 删除尾节点(但非唯一):next == nulllast 更新为原倒数第二节点,prev.next 被置 null

remove(Object) 特别逻辑

  • 分开处理 o == nullo != null,避免在 equals() 中出现空指针。
  • 遍历定位后仅移除第一个匹配项,若需全删需外部循环。

模块 8:查询操作(get / indexOf)源码剖析

get 实现完全依赖 node(index):

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

get 流程图与二分方向决策:

flowchart TD
    S["get(index)"] --> CHECK["checkElementIndex(index)"]
    CHECK --> HALF{"index < size >> 1 ?"}
    HALF -->|是| FORWARD["x = first<br/>for(i=0; i<index; i++) x=x.next"]
    HALF -->|否| BACKWARD["x = last<br/>for(i=size-1; i>index; i--) x=x.prev"]
    FORWARD --> RET["return x.item"]
    BACKWARD --> RET

图 8.1 — get 流程图说明

执行步骤:

  1. checkElementIndex(index) 确保 index[0, size-1] 范围内。
  2. 二分判断 index 位于前半段还是后半段:
    • 前半段从 first 出发,循环 index 次到达目标节点。
    • 后半段从 last 出发,反向移动 (size-1-index) 步。
  3. 返回 x.item

关键点:

  • 即使二分优化,最坏仍需 size/2 次指针追踪。
  • 没有像 ArrayList 的随机访问优化,因其元素内存不连续,无法通过偏移量直接计算地址。

indexOf 源码片段:

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

从头至尾线性遍历,时间复杂度 O(n),且不做二分优化,因为无法预测目标位置。


Part 4:双端队列与迭代篇

模块 9:Deque 接口实现——栈与队列操作

LinkedList 实现了 Deque,因此所有栈、队列、双端操作方法均映射到内部 add/remove 系列方法。

方法映射关系图:

flowchart LR
    subgraph Stack_Ops["栈操作"]
        PUSH["push(e)"] --> AF["addFirst(e)"]
        POP["pop()"] --> RF["removeFirst()"]
    end
    subgraph Queue_Ops["队列操作"]
        OFFER["offer(e)"] --> AL["addLast(e)"]
        POLL["poll()"] --> P_F["pollFirst() → unlinkFirst"]
        PEEK["peek()"] --> PK_F["peekFirst() → getFirst"]
    end
    subgraph Deque_Ops["双端操作"]
        OFF_F["offerFirst(e)"] --> AF2["addFirst(e)"]
        OFF_L["offerLast(e)"] --> AL2["addLast(e)"]
        POLL_F["pollFirst()"] --> RF2["removeFirst()"]
        POLL_L["pollLast()"] --> RL["removeLast()"]
    end

图 9.1 — Deque 方法映射层次说明

分步骤解读:

  • 栈 (LIFO)push() 调用 addFirst(),将元素压入顶部;pop() 调用 removeFirst() 移除顶部元素。符合栈的“后进先出”语义。
  • 队列 (FIFO)offer() 内部调用 addLast() 入队到尾部;poll() 调用 unlinkFirst() 从头部出队;peek() 访问头部元素但不移除。
  • 双端队列offerFirst/offerLast 分别映射 addFirst/addLastpollFirst/pollLast 映射 removeFirst/removeLast
  • 恒为 O(1):由于 LinkedList 无容量限制,offer 系方法永远不会返回 falsepoll 在空表时返回 null,而 remove 抛出异常。

为何做栈/队列时推荐 ArrayDeque 而非 LinkedList?

  • 内存开销:ArrayDeque 基于循环数组,无额外 Node 对象头,空间节约、GC 压力小。
  • 缓存局部性:数组元素连续存放,CPU 缓存命中率高;LinkedList 节点散落,每次访问可能 cache miss。
  • 不允许 null:ArrayDeque 禁止 null 元素,若业务数据包含 null 才需 LinkedList。
  • 结论:除特殊情况(null 元素、需要 List 接口)外,对于栈和队列,ArrayDeque 是更快更省内存的选择。

模块 10:迭代器深度剖析——fail-fast 与链表遍历

LinkedList 的迭代器由内部类 ListItr 实现,支持双向遍历和在迭代过程中的增删改。

核心字段(简写):

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;    // 刚返回的节点
    private Node<E> next;           // 下一次 next() 返回的节点
    private int nextIndex;          // next 的索引
    private int expectedModCount = modCount;
    ...
}

next() 与 remove() 协同时序图:

sequenceDiagram
    participant Client
    participant ListItr
    participant LinkedList
    Client->>ListItr: next()
    ListItr->>ListItr: checkForComodification()
    ListItr->>LinkedList: next节点
    ListItr->>ListItr: lastReturned = next<br/>next = next.next<br/>nextIndex++
    ListItr-->>Client: element
    Client->>ListItr: remove()
    ListItr->>ListItr: checkForComodification()
    alt lastReturned == null
        ListItr-->>Client: 抛 IllegalStateException
    end
    ListItr->>LinkedList: unlink(lastReturned)
    LinkedList-->>ListItr: void
    ListItr->>ListItr: if (next == lastReturned)<br/>    next = lastReturned.next<br/>else nextIndex--<br/>lastReturned = null<br/>expectedModCount = modCount
    ListItr-->>Client: 完成删除

图 10.1 — 迭代器 next/remove 交互说明

分步骤详细解析:

next() 流程

  1. checkForComodification() 比对 modCountexpectedModCount,若不等立即抛出 ConcurrentModificationException
  2. 获取 next 指向的节点,赋给 lastReturned
  3. next 后移至 next.nextnextIndex++
  4. 返回 lastReturned.item

remove() 流程

  1. checkForComodification() 再次检查并发修改。
  2. lastReturned == null,表明调用者未先调用 next()previous(),或已重复调用 remove(),直接抛出 IllegalStateException
  3. 调用 LinkedList.this.unlink(lastReturned) 执行底层节点移除。此时 modCount 会自增。
  4. 同步迭代器状态
    • next == lastReturned(例如刚刚 previous() 后就 remove()),则 next 需后移到 lastReturned.next
    • 否则 nextIndex--(因为删除了 next 前面的元素)。
    • lastReturned = null,防止重复删除。
    • expectedModCount 更新为新的 modCount,使得接下来迭代器操作不再抛异常。

关键点提炼:

  • fail-fast 机制:任何直接调用集合的结构性修改(add/remove 等)都会递增 modCount,导致所有活跃迭代器的 expectedModCount 不匹配,从而在下一次迭代器操作时抛出异常。
  • 迭代器自身 remove/add 在修改集合后会同步 expectedModCount = modCount,因此合法修改不会导致自身抛异常。
  • 单向遍历陷阱:如果在 for-each(本质是迭代器)中使用 list.remove() 会立即抛出 ConcurrentModificationException

边界条件分析:

  • 空链表hasNext() 返回 false,调用 next() 抛出 NoSuchElementException
  • 单元素迭代:调用 next() 返回唯一元素,之后 hasNext() 变为 false;若紧接着 remove(),链表变空,first == last == null

模块 11:序列化与 transient 设计

LinkedList 的 firstlast 字段标记为 transient,序列化时并非直接保存整个节点图,而是采用 按元素逐步写入 的策略。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    s.defaultWriteObject(); // 写入 size 等非 transient 字段
    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(); // 读取 size
    int size = s.readInt(); // 实际上是读取了 size,但这里源码直接循环 readObject
    for (int i = 0; i < size; i++)
        linkLast((E) s.readObject());
}

序列化/反序列化时序图:

sequenceDiagram
    participant OOS as ObjectOutputStream
    participant LL as LinkedList
    participant Stream as 底层流
    OOS->>LL: writeObject()
    LL->>LL: defaultWriteObject() (写入 size 等字段)
    LL->>LL: Node x = first
    loop 每个元素
        LL->>Stream: writeObject(x.item)
        LL->>LL: x = x.next
    end
    Note over OOS,Stream: 序列化完成
    participant OIS as ObjectInputStream
    OIS->>LL: readObject()
    LL->>LL: defaultReadObject() (读取 size)
    LL->>LL: 读取 size 值
    loop size 次
        OIS->>LL: readObject() 得到元素
        LL->>LL: linkLast(element)
    end

图 11.1 — 序列化流程说明

分步骤对应:

  1. 序列化开始ObjectOutputStream 调用 LinkedList.writeObject
  2. 写入非 transient 字段s.defaultWriteObject()size 写入流(size 并非 transient,注意 first/last 是 transient,不写入)。
  3. 逐元素写入:从 first 出发遍历每个节点,s.writeObject(x.item) 仅写入数据本身,忽略 prev/next 指针。
  4. 反序列化readObject() 先通过 defaultReadObject() 恢复 size,然后循环 size 次,依次 linkLast 重建链表。

设计意图:

  • 跨平台兼容:序列化结果仅包含元素顺序,不暴露内部 Node 结构,未来版本改变内部实现不会破坏序列化兼容性。
  • 节省空间:避免将 prev/next 指针写入流,尤其在分布式环境中减少传输数据量。
  • 安全性:防止序列化攻击利用底层链表结构。

边界条件:

  • 空链表size == 0writeObject 仅写入 size,循环不执行;readObject 同样不执行 linkLast,恢复后链表为空。

Part 5:对比与并发篇

模块 12:LinkedList 与 ArrayList 全方位对比

两者在底层数据结构、性能开销和适用场景上截然相反。

操作时间复杂度对比:

操作ArrayListLinkedList
get(int)O(1)O(n)
add(E)(尾部)均摊 O(1)O(1)
add(int, E)O(n)(系统数组拷贝)O(n)(定位)
remove(int)O(n)O(n)
remove(Object)O(n)O(n)
addFirst/removeFirst需拷贝,O(n)O(1)

内存与缓存特性:

  • ArrayList:内部 Object[] 连续存储,只有引用类型时每个元素占一个引用宽度;扩容时产生一次性拷贝开销;对 CPU 缓存极其友好。
  • LinkedList:每个元素额外需要两个指针(共 24 字节开销,各 JVM 有差异),且节点分散在堆各处,导致迭代过程中 CPU cache miss 明显增加。

选型决策树:

graph TD
    START["开始选型"] --> Q1{"主要操作包含大量索引随机访问?"}
    Q1 -->|"是"| R_ARRAY["ArrayList"]
    Q1 -->|"否"| Q2{"增删集中在头部或尾部?"}
    Q2 -->|"是"| Q3{"需要 null 元素或 List 接口操作?"}
    Q3 -->|"是"| R_LINK["LinkedList"]
    Q3 -->|"否"| R_ARDEQ["ArrayDeque"]
    Q2 -->|"否"| Q4{"频繁在中间位置插入删除?"}
    Q4 -->|"是"| Q5{"集合规模通常多大?"}
    Q5 -->|"大(> 数十万)"| R_LINK2["LinkedList(减少数组拷贝)"]
    Q5 -->|"中小"| R_ARRAY2["ArrayList(拷贝开销可接受)"]
    Q4 -->|"否"| R_ARRAY3["ArrayList"]

图 12.1 — 选型决策树详细说明

决策步骤解读:

  • 步骤 1:大量索引随机访问 → 直接选 ArrayList,LinkedList 的 O(n) 访问不可接受。
  • 步骤 2:增删集中在头部或尾部 → 若同时需要 null 或 List 接口功能,选 LinkedList;否则 ArrayDeque 性能更佳。
  • 步骤 3:中间位置频繁插入删除 → 需权衡尺寸。链表定位虽慢,但数组拷贝的代价随元素数量线性增长。当元素极多(如 100 万级),拷贝整个数组的成本可能高于链表遍历;但一般业务中,ArrayList 的 System.arraycopy 实现极其高效,多数场景仍表现良好。
  • 反例:绝不可在大量随机访问场景使用 LinkedList。

关键点提炼:

  • 确定性场景:读多写少、按索引访问 → ArrayList;频繁头尾增删、无随机访问 → LinkedList / ArrayDeque。
  • 混合场景建议通过实际压测决定,不唯理论复杂度。

模块 13:并发问题与线程安全方案

非线程安全的根本原因:
LinkedList 所有修改操作均未采用同步手段。多个线程同时修改 first/last 或内部节点指针可能导致:

  1. 节点丢失:两个线程同时执行 addFirst,可能互相覆盖 first 指针,导致之前插入的节点脱离链表。
  2. 链表环路:指针更新交叉可能导致 next / prev 形成回路,引发遍历死循环。
  3. size 不一致:非原子递增导致元素个数与实际节点数不符。
  4. fail-fast 抛出:并发修改会触发 ConcurrentModificationException

多线程并发 addFirst 故障示意:

sequenceDiagram
    participant T1 as Thread-1
    participant T2 as Thread-2
    participant LIST as LinkedList
    Note over LIST: 初始 first=null, last=null
    T1->>LIST: addFirst("A")<br/>读取 first=f (null)
    T2->>LIST: addFirst("B")<br/>读取 first=f (null)<br/>newNodeB, first=newNodeB<br/>f==null, last=newNodeB
    Note over LIST: 此时 first/ last 均指向节点B
    T1->>LIST: 创建newNodeA (prev=null,next=null)<br/>first=newNodeA, f==null → last=newNodeA
    Note over LIST: first 指向 A, last 指向 A<br/>节点 B 丢失,且无引用

图 13.1 — 多线程故障说明

层次说明:

  • Thread-1 和 Thread-2 同时进入 addFirst,均获得当前 first == null
  • Thread-2 率先完成 linkFirst("B")firstlast 都指向节点 B。
  • Thread-1 继续执行,重新将 firstlast 指向节点 A,节点 B 丢失
  • 这就是典型竞态条件,最终链表被破坏。

三种线程安全 List 对比:

实现同步策略迭代器特性适用场景
Vector方法级 synchronized较旧,枚举器不 fail-fast遗留系统兼容,不推荐新代码
Collections.synchronizedList(new LinkedList())同步包装器,每个方法内在 mutex 上同步迭代器需要外部同步低并发替代方案
CopyOnWriteArrayList写时复制整个数组,读无锁迭代器不受修改影响,弱一致性读多写少,元素总量不大

为何不推荐用同步包装的 LinkedList 做并发队列:

  1. 粗粒度锁synchronizedList 每个操作都持有同一个锁,并发吞吐量极低,尤其迭代期间锁定整个集合。
  2. 复合操作风险:非原子操作(如 if(!list.isEmpty()) list.removeFirst())需额外同步,否则竞态依然存在。
  3. 专业并发队列替代ConcurrentLinkedQueue(无界非阻塞)和 LinkedBlockingQueue(有界阻塞)采用 CAS 或双锁设计,支持更高并发且天然保证线程安全。若需双端队列,有 ConcurrentLinkedDequeLinkedBlockingDeque

加分建议:

  • 多数并发队列场景无需 List 接口,直接使用 Queue/Deque 的专业实现即可获得极致性能。
  • 若必需 List 语义且读多写少,CopyOnWriteArrayList 是更好的线程安全选择,但写开销极大。

Part 6:总结与面试篇

模块 14:注意事项与最佳实践

反面案例 1:索引循环随机访问
for (int i = 0; i < list.size(); i++) { list.get(i); }
每次 get 都从头/尾遍历,时间复杂度 O(n²)。
✅ 使用增强 for 或 Iterator,全程只需一次顺序遍历。

反面案例 2:for-each 中直接删除
for (String s : list) { if (s.equals("del")) list.remove(s); }
抛出 ConcurrentModificationException
✅ 显式使用迭代器:Iterator<String> it = list.iterator(); while(it.hasNext()){ if(it.next().equals("del")) it.remove(); }

反面案例 3:同步要求下裸用 LinkedList
❌ 多线程共享一个 LinkedList,无任何同步机制。
✅ 使用 Collections.synchronizedList 并记得迭代时同步锁定,或直接改用并发集合。

最佳实践摘要:

  • 遍历:永远用迭代器或增强 for。
  • 批量导入addAll 优于循环 add,减少定位次数。
  • 栈/队列选择:优先 ArrayDeque,除非需要 null 或 List 索引功能。
  • 删除条件复杂时:使用 removeIf(JDK8+)或 Iterator
  • 序列化:注意 transient 字段,避免直接序列化 Node 导致版本兼容问题。

模块 15:性能总结与选型建议

时间复杂度速查表:

方法时间复杂度备注
addFirst / addLastO(1)直接调整指针
removeFirst / removeLastO(1)直接摘除头尾节点
get(int)O(n)二分方向优化,平均 n/4
add(int, E)O(n)定位 O(n),插入 O(1)
remove(int)O(n)定位 + 摘除
indexOf / remove(Object)O(n)全链表遍历
迭代器 next/removeO(1) / O(1)仅修改指针

选型边界:

  • 集合规模较小(< 1000)且操作混合时,ArrayList 和 LinkedList 实际性能差异常可忽略,但 ArrayList 内存占用更优。
  • 头尾操作频率占比超过 80%,且不涉及索引查找,LinkedList 价值凸显。
  • 内存压力大且元素数量庞大,LinkedList 的节点开销可能成为致命伤。

模块 16:面试高频专题

以下精选 10 个必考问题,均附带标准回答、追问及加分回答。


1. LinkedList 底层使用什么数据结构?

标准回答:
底层采用双向链表,内部定义静态内部类 Node<E>,包含元素 item 以及前驱 prev 和后继 next 指针。LinkedList 自身维护 firstlast 两个引用分别指向头尾节点。

追问:为什么选择双向链表而非单向链表?

  • 单向链表无法在 O(1) 时间内删除尾部节点,因为需要遍历找到前驱。双向链表可通过 last.prev 直接定位,实现双端队列的高效操作。

加分回答:
AbstractSequentialList 的设计天然要求支持双向遍历的 ListIterator。单向链表无法高效实现 hasPrevious()previous(),因此不能用于满足 List 接口的全部契约。


2. addFirst 与 addLast 如何保证 O(1)?

标准回答:
addFirst 调用 linkFirst,只需新建节点、重新赋值 first 指针,若原链表为空同步更新 last,否则将原头节点的 prev 指向新节点。不涉及遍历,所以是 O(1)。addLast 镜像操作。

追问:如果是空链表,addFirst 会影响 last 吗?

  • 会。linkFirstif (f == null) last = newNode; 保证插入第一个节点后,firstlast 指向同一节点。

加分回答:
可以对比 ArrayListadd(0, e),需要拷贝整个数组,复杂度 O(n),体现了链表在头部插入的绝对优势。


3. LinkedList 随机访问为什么慢?

标准回答:
get(index) 通过 node(index) 方法从头或尾逐个遍历直到目标位置,时间复杂度 O(n)。即使通过 size >> 1 进行二分方向优化,也只是将常数因子减半,无法改变线性本质。

追问:CPU 缓存对遍历的影响?

  • ArrayList 的数组连续存储,空间局部性好,CPU 能预取缓存行;LinkedList 节点内存分散,每一次指针跳转都可能触发 cache miss,实际性能差异比理论更大。

加分回答:
在存在大量随机访问的场合,即使 LinkedList 实现了 List 接口,也应视为顺序访问集合,否则性能会急剧下降。


4. ArrayList 和 LinkedList 的使用场景有何区别?

标准回答:

  • ArrayList 适合频繁随机读取、尾部追加场景,内存连续、缓存友好。
  • LinkedList 适合频繁头尾增删、无需索引访问的场景,也可用于实现栈和队列。

追问:中间插入何时 LinkedList 可能更优?
当元素数量极大(百万级),ArrayList 的数组拷贝成本可能高于链表遍历加指针修改的开销,但一般需实际压测确定。

加分回答:
实际开发中,ArrayList 的覆盖场景远广于 LinkedList,后者常用于算法题(如 LRU 缓存、约瑟夫环)或因特殊 Deque 需求。


5. LinkedList 如何实现栈和队列?

标准回答:
LinkedList 实现了 Deque 接口。

  • 作为栈:push() 映射 addFirst()pop() 映射 removeFirst()
  • 作为队列:offer() 映射 addLast()poll() 映射 removeFirst()
    全部操作 O(1)。

追问:用它做队列有什么缺点?
内存开销大,不支持界满阻塞,不适合高并发生产者-消费者模型。应使用 ArrayDeque(单机高效)或 LinkedBlockingQueue(并发阻塞)。

加分回答:
常规栈和队列首选 ArrayDeque,其循环数组节省空间且 CPU 友好,仅当业务数据包含 null 或必须使用 List 接口时才回退到 LinkedList。


6. 迭代器 fail-fast 是如何实现的?

标准回答:
LinkedList 维护 modCount 字段,每次结构性修改都会递增。迭代器创建时记录 expectedModCount = modCount。任何操作前调用 checkForComodification() 比对两者,若不匹配立即抛出 ConcurrentModificationException

追问:迭代器自身删除为什么不会抛异常?
迭代器的 remove() 在调用 unlink 后会更新 expectedModCount = modCount,使之与集合的最新修改计数一致。

加分回答:
fail-fast 行为仅能尽力检测并发修改,不能保证在所有情况下都正确抛异常,因此不能将其作为并发控制手段。必须通过同步机制保证线程安全。


7. LinkedList 是线程安全的吗?如何实现线程安全的 List?

标准回答:
LinkedList 完全非线程安全。可通过 Collections.synchronizedList(new LinkedList<>()) 获取同步包装器,所有方法加入互斥锁。但迭代期间仍需手动同步。

追问:Vector 和同步包装器区别?
Vector 是遗留类,方法自带 synchronized;同步包装器使用内部 mutex,更灵活。但两者都是粗粒度锁,并发性能差。

加分回答:
更现代的选择是根据场景使用 CopyOnWriteArrayList(读多写少)或直接改用并发队列/双端队列如 ConcurrentLinkedDeque,它们采用无锁 CAS 等机制,显著提升并发吞吐。


8. 序列化时为什么 first/last 用 transient?如何重新还原链表?

标准回答:
firstlast 被标记为 transient,避免序列化整个 Node 图。writeObject 仅循环写入每个元素的 itemreadObject 在反序列化时读取元素并依次调用 linkLast 重建链表,保证了版本兼容和流格式简洁。

追问:如果误使用默认序列化会有什么后果?
可能序列化大量冗余指针信息,且当 JDK 内部 Node 结构变化时,不同版本间无法兼容反序列化。

加分回答:
size 并未标记 transient,因此可由 defaultWriteObject 直接写入流,为反序列化时的循环次数提供依据,减少了额外写入长度的需要。


9. remove(Object) 删除元素具体流程?

标准回答:
first 开始依次遍历节点,对 null 使用 ==、对非 null 使用 equals 匹配。找到第一个匹配节点后调用 unlink(x) 将其从链表摘除,x.prevx.next 重新互联,置空 x.item 并更新 first/last 如需要,最后 size--modCount++

追问:如果链表中有多个相同对象,remove 会全部删除吗?
不会,仅移除第一个匹配节点。如需全删,应使用 removeIfwhile(list.remove(obj))

加分回答:
unlink 的指针操作顺序经过精心设计,先链接前后节点、再断开自身指针,有效防止遍历过程中出现断链;同时将 x.itemnull 帮助 GC 快速回收大对象。


10. 什么情况下你会坚持使用 LinkedList?

标准回答:

  • 需要频繁在 List 头部插入或删除元素。
  • 需要同时使用 List 与 Deque 的功能,且不引入额外类。
  • 数据集中包含 null 而其他双端队列(如 ArrayDeque)不支持。

追问:能举出一个典型应用吗?
LRU 缓存的简单实现中,需要将最近访问的元素移至头部并淘汰尾部(removeLast),LinkedList 的 remove(Object) + addFirst 完美匹配,时间复杂度均可接受。

加分回答:
在不需要随机访问且集合生命周期中频繁发生结构性变化的场景,LinkedList 避免数组复制的峰值延时,有利于控制 GC 停顿(无大数组一次性回收)。但实际选型仍需根据 JVM 行为和服务需求权衡。