LinkedList 深入解析

在 Java 集合框架中,LinkedList 是 List 接口与 Deque 接口的双实现类,底层基于双向链表实现,凭借高效的插入、删除特性,成为 ArrayList 的核心互补集合。

一、LinkedList 基础定义

LinkedList 是java.util包下的双向链表 实现类,同时实现了ListDeque(双端队列)接口,既具备 List 的有序存储特性,又拥有队列、双端队列、栈的功能特性。

核心特性:

  1. 双向链表结构:每个节点存储数据、前驱节点、后继节点,内存非连续存储;
  2. 有序可重复:元素存储顺序与插入顺序一致,允许存储 null 值和重复元素;
  3. 无固定容量:无需扩容,节点动态创建,理论容量无上限;
  4. 非线程安全:多线程并发修改会引发数据异常;
  5. 随机访问低效:不支持通过索引直接访问,查找元素需遍历链表。

二、LinkedList 的继承与实现关系

LinkedList 的类定义源码清晰展示了其完整继承体系,这是理解其功能的基础:

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

核心继承 / 实现关系解析

  1. 继承AbstractSequentialList :抽象父类,专为顺序访问的集合设计(如链表),实现了基于迭代器的通用方法,与 ArrayList 继承的AbstractList形成区分;
  2. 实现List接口:遵循列表规范,提供增删改查、索引操作等核心功能;
  3. 实现Deque接口核心特性,具备双端队列能力,支持头 / 尾快速增删元素,可作为栈、队列、双端队列使用;
  4. 实现Cloneable接口:支持浅克隆;
  5. 实现Serializable接口:支持序列化,可网络传输与持久化。

关键区别:ArrayList 继承AbstractList支持随机访问,LinkedList 继承AbstractSequentialList仅支持顺序访问。

三、LinkedList 源码解析

LinkedList 的核心是双向链表节点设计头尾节点管理节点操作逻辑,无扩容机制,源码比 ArrayList 更简洁易懂。

1. 核心成员变量

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    
    // 链表元素个数
    transient int size = 0;

    /**
     * 链表头节点(第一个节点)
     */
    transient Node<E> first;

    /**
     * 链表尾节点(最后一个节点)
     */
    transient Node<E> last;
    
    // 序列化版本号
    private static final long serialVersionUID = 876323262645176354L;
}

2. 核心内部类:Node(双向链表节点)

LinkedList 的存储单元是Node 节点,这是双向链表的核心,源码如下:

java 复制代码
private static class Node<E> {
    // 存储的元素数据
    E item;
    // 后继节点(指向后一个元素)
    Node<E> next;
    // 前驱节点(指向前一个元素)
    Node<E> prev;

    // 节点构造方法:传入前驱、数据、后继,完成节点关联
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

节点结构图解prev节点 ← [item, prev, next] → next节点每个节点都能找到前后节点,这是双向链表的核心优势。

3. 构造方法

LinkedList 提供两个极简构造方法,无容量参数(无需扩容):

java 复制代码
/**
 * 无参构造:创建空链表,头尾节点均为null
 */
public LinkedList() {
}

/**
 * 集合参数构造:将指定集合元素添加到链表中
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

对比:ArrayList 无参构造会初始化空数组,LinkedList 无参构造直接为空链表,无任何内存分配。

4. 核心节点操作工具方法

LinkedList 所有增删改查,底层都是头节点、尾节点、中间节点的关联操作,核心封装了 4 个工具方法:

(1)linkFirst:将元素添加到链表头部
java 复制代码
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)
        last = newNode;
    else
        // 原头节点的前驱指向新节点
        f.prev = newNode;
    size++;
    modCount++;
}
(2)linkLast:将元素添加到链表尾部(默认 add 方法调用)
java 复制代码
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)
        first = newNode;
    else
        // 原尾节点的后继指向新节点
        l.next = newNode;
    size++;
    modCount++;
}
(3)linkBefore:在指定节点前插入元素(中间插入)
java 复制代码
void linkBefore(E e, Node<E> succ) {
    // 获取指定节点的前驱节点
    final Node<E> pred = succ.prev;
    // 创建新节点:前驱=pred,后继=succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 指定节点的前驱指向新节点
    succ.prev = newNode;
    // 如果前驱为null,新节点为头节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
(4)unlink:删除指定节点(核心删除方法)
java 复制代码
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) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null; // 断开前驱引用,帮助GC
    }

    // 如果是尾节点,删除后前驱节点成为新尾节点
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null; // 断开后继引用,帮助GC
    }

    x.item = null; // 数据置空
    size--;
    modCount++;
    return element;
}

四、LinkedList 核心方法源码解析

基于上述节点工具方法,LinkedList 实现了 List 和 Deque 的所有核心方法,逻辑高度统一。

1. add () 添加元素(最常用)

java 复制代码
// 默认添加到尾部
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 指定索引位置添加
public void add(int index, E element) {
    checkPositionIndex(index);
    // 如果是尾部,直接调用linkLast
    if (index == size)
        linkLast(element);
    else
        // 中间插入:找到index对应节点,调用linkBefore
        linkBefore(element, node(index));
}

2. node () 查找指定索引的节点(随机访问低效根源)

java 复制代码
Node<E> node(int index) {
    // 二分优化:判断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;
    }
}

优化点:JDK 做了二分遍历优化,但时间复杂度仍为 O (n),远慢于 ArrayList 的 O (1)。

3. get () 获取元素

java 复制代码
public E get(int index) {
    checkElementIndex(index);
    // 调用node方法遍历查找节点,返回数据
    return node(index).item;
}

4. remove () 删除元素

java 复制代码
// 根据索引删除
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

// 根据元素删除
public boolean remove(Object o) {
    if (o == null) {
        // 遍历链表删除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;
}

5. Deque 接口方法(栈 / 队列专用)

LinkedList 可直接作为栈、队列使用,无需额外实现:

java 复制代码
// 入队/入栈
public void push(E e) { addFirst(e); }
// 出队/出栈
public E pop() { return removeFirst(); }
// 获取队头/栈顶元素
public E peek() { return (first == null) ? null : first.item; }

五、LinkedList 迭代器原理

LinkedList 没有实现RandomAccess接口,只能使用迭代器 / 增强 for 循环遍历(普通 for 循环效率极低),迭代器基于双向链表顺序访问实现。

1. 核心迭代器源码(ListItr)

java 复制代码
private class ListItr implements ListIterator<E> {
    // 上一个返回的节点
    private Node<E> lastReturned;
    // 下一个要遍历的节点
    private Node<E> next;
    // 下一个节点索引
    private int nextIndex;
    // 快速失败标记
    private int expectedModCount = modCount;

    // 判断是否有下一个元素
    public boolean hasNext() {
        return nextIndex < size;
    }

    // 获取下一个元素(顺序遍历)
    public E next() {
        checkForComodification();
        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
}

2. 快速失败机制(Fail-Fast)

与 ArrayList 完全一致:

  • modCount:链表修改次数(增 / 删都会 + 1);
  • 迭代器初始化时会拷贝modCountexpectedModCount
  • 迭代过程中如果链表被外部修改,两个值不相等,立即抛出ConcurrentModificationException

3. 遍历建议

  1. 禁止使用普通 for 循环:每次 get (index) 都会从头遍历链表,效率极差;
  2. 优先使用迭代器 / 增强 for 循环:顺序遍历,时间复杂度 O (n),性能最优。

六、LinkedList 与 ArrayList 对比

两者底层结构、性能、适用场景完全互补:

对比维度 ArrayList LinkedList
底层结构 动态 Object 数组(内存连续) 双向链表(内存非连续)
随机访问 支持,O (1) 时间复杂度,效率极高 不支持,O (n) 时间复杂度,效率低
插入 / 删除 末尾操作 O (1),中间操作 O (n)(需移动元素) 头 / 尾操作 O (1),中间操作 O (n)(仅修改引用)
扩容机制 1.5 倍动态扩容,需数组复制 无扩容,节点动态创建
内存占用 内存紧凑,扩容会产生空闲空间 每个节点存储数据 + 前后指针,内存开销更大
遍历方式 普通 for / 迭代器均可 仅推荐迭代器 / 增强 for 循环
继承父类 AbstractList(支持随机访问) AbstractSequentialList(顺序访问)
实现接口 List、RandomAccess List、Deque(双端队列)
线程安全 非线程安全 非线程安全
适用场景 大量查询、读取操作 大量头 / 尾插入、删除操作

总结

  1. 读多写少、频繁随机访问 → ArrayList(开发首选);
  2. 频繁头尾增删、作为栈 / 队列使用 → LinkedList
  3. 两者均非线程安全,多线程环境使用CopyOnWriteArrayList

七、LinkedList 面试高频考点

1. LinkedList 的底层结构是什么?

基于双向链表 实现,每个节点包含item(数据)、prev(前驱节点)、next(后继节点)。

2. LinkedList 为什么不需要扩容?

链表无固定容量,新增元素时动态创建 Node 节点,通过引用关联,无需像数组一样重新分配内存。

3. LinkedList 插入 / 删除为什么比 ArrayList 快?

  • ArrayList:中间插入 / 删除需要移动后续所有元素,时间复杂度 O (n);
  • LinkedList:仅需修改节点的前驱 / 后继引用,无需移动元素,头 / 尾操作时间复杂度 O (1)。

4. LinkedList 为什么随机访问效率低?

不支持索引直接访问,查找元素需要从头 / 尾遍历链表,即使 JDK 做了二分优化,时间复杂度仍为 O (n)。

5. LinkedList 可以作为哪些数据结构使用?

同时实现了 Deque 接口,可作为:List(列表)、栈(Stack)、队列(Queue)、双端队列(Deque)

6. LinkedList 和 ArrayList 的核心区别?

核心区别是底层结构:数组 vs 双向链表,由此衍生出访问效率、增删效率、内存占用、适用场景的全部差异。

7. LinkedList 遍历为什么不推荐用普通 for 循环?

普通 for 循环每次调用get(index)都会重新遍历链表,时间复杂度 O (n²),性能极差。

8. LinkedList 是线程安全的吗?如何保证安全?

非线程安全;多线程环境可使用Collections.synchronizedList包装,或使用 JUC 包的ConcurrentLinkedQueue(并发链表)。

八、总结

LinkedList 是基于双向链表设计的多功能集合,凭借无扩容、头尾操作高效、支持栈 / 队列特性,成为 ArrayList 的完美互补。

核心记忆点:

  1. 底层:双向链表,无扩容,节点动态创建;
  2. 优势:头尾增删极快,支持多种数据结构;
  3. 劣势:随机访问低效,内存开销大;
  4. 场景:频繁头尾操作、栈 / 队列需求。
相关推荐
鱼鳞_1 小时前
Java学习笔记_Day32(IO流字符集字符流)
java·笔记·学习
Rsun045511 小时前
17、Java 责任链模式从入门到实战
java·python·责任链模式
GDAL1 小时前
IntelliJ IDEA 前端开发全面教程
java·ide·intellij-idea
rabbit_pro1 小时前
IDEA保存SSH会话信息
java·ssh·intellij-idea
Rsun045511 小时前
12、Java 享元模式从入门到实战
java·开发语言·享元模式
小则又沐风a2 小时前
深剖string内部结构 手撕string
java·前端·数据库·c++
2401_832635582 小时前
Spring Data MongoDB 最佳实践:如何构建高效数据访问层
java·mongodb·spring
亚马逊云开发者2 小时前
Java 8升级Java 17实战:用AWS Transform Custom自动化迁移Spring Boot项目完整教程
java·自动化·aws
CHANG_THE_WORLD2 小时前
C 语言的 `fread` 与 C++ 的 `ifstream::read` 区别及设计哲学
java·c语言·c++