LinkedList 源码分析

本文首发于公众号:JavaArchJourney

LinkedList介绍

LinkedList特点如下:

  • 基于双向链表实现LinkedList 的实现数据结构是双向链表,链表由一系列Node节点组成,每个Node包含数据、指向前一个Node的引用、指向后一个Node的引用。链表的大小是动态的,可以根据需要增加或减少节点。
  • 高效的插入和删除:在链表中插入或删除元素通常只需要改变相关节点的引用,这使得操作效率较高。
  • 访问效率低 :与数组不同,链表不支持随机访问。获取链表中特定位置的元素需要从头/尾开始遍历链表直到找到目标位置,时间复杂度为 O(n) ,效率较低。
  • 额外的内存开销 :由于每个节点需要存储指向前一个节点和后一个节点的引用,因此相比基于数组的实现( ArrayList ),链表会消耗更多的内存。
  • 不需要连续的内存空间:链表不像数组那样需要一块连续的内存空间,它可以利用分散的内存区域存储数据。

LinkedList的类继承结构如下:

  • 继承自AbstractSequentialListAbstractSequentialList 要求子类必须覆盖的方法包括 listIterator()size()。其中 listIterator() 方法用于返回列表迭代器,允许对列表进行遍历、添加、删除和修改等操作;而 size() 方法则需要返回列表中元素的数量。AbstractSequentialList 通过依赖列表迭代器实现对列表的操作,为适合顺序访问的列表数据结构提供了一个简化实现的抽象框架。因此,继承自 AbstractSequentialList 的类更适合顺序访问,而非随机访问。
  • 实现了 List<E> 接口List 接口定义了允许重复元素的有序集合(即列表)操作,,支持根据索引进行元素访问、添加、删除等操作。
  • 实现了 Deque 接口 :表示 LinkedList 能够在两端 高效地添加和移除元素,既可以用作队列 也可以用作
  • 实现了 Cloneable 接口 :表示支持对象的克隆操作。LinkedList 提供了 clone() 方法来创建当前列表的一个浅拷贝。
  • 实现了 Serializable 接口 :表示 LinkedList 支持序列化操作,可用于对象的持久化存储或网络传输。

LinkedList源码分析

jdk 1.8 版本为例,LinkedList 源码的核心方法剖析如下:

存储结构

基本属性:

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

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

双向链表结构图示:

构造链表

java 复制代码
/**
 * 默认构造函数:构造空链表,就是什么都不做
 */
public LinkedList() {
}

/**
 * 根据指定集合构造链表(顺序由集合的Iterator提供)
 */
public LinkedList(Collection<? extends E> c) {
    this();
    // 把集合c所有元素逐个插入链表中
    addAll(c);
}

public boolean addAll(Collection<? extends E> c) {
    // 从size位置开始(即链表尾部开始),插入集合c的所有元素
    return addAll(size, c);
}

/**
 * 从下标为的index的地方开始,把指定集合c中的所有元素插入链表
 */
public boolean addAll(int index, Collection<? extends E> c) {
    // 检查index是否越界(在 [0,size] 闭区间内),若越界则抛出IndexOutOfBoundsException异常
    checkPositionIndex(index);

    // 集合转为数组,方便逐个操作
    Object[] a = c.toArray();
    // 待添加元素的数量
    int numNew = a.length;
    // 若待添加元素数量为0,则不增加,并返回false
    if (numNew == 0)
        return false;

    // 找到待插入位置(index位置)的前驱与后继节点
    Node<E> pred, succ;
    if (index == size) {
        // 若在链表尾部追加数据:
        // 后继节点是null
        succ = null;
        // 前驱节点是当前的尾节点
        pred = last;
    } else {
        // 后继节点是当前index位置的节点
        succ = node(index);
        // 前驱节点是当前index位置的前驱节点
        pred = succ.prev;
    }

    // for循环遍历待添加元素的数组,依次执行插入节点操作
    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
    size += numNew;
    // 更新modeCount
    modCount++;
    return true;
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

/**
 * 遍历链表找到index位置的节点
 */
Node<E> node(int index) {
    // 若 index < size/2,就从位置0往后遍历到位置index处
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    }
    // 若 index >= size/2,就从位置size往前遍历到位置index处
    else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

操作方法

插入节点

java 复制代码
    /**
     * 在指定位置插入一个元素
     */
    public void add(int index, E element) {
        // 检查index,越界则抛出 IndexOutOfBoundsException 异常
        checkPositionIndex(index);

        if (index == size)
            // 在尾部插入
            linkLast(element);
        else
            // 在中间插入:找到当前 index 位置节点,在其前面插入
            linkBefore(element, node(index));
    }

    /**
     * 在链表尾部插入一个元素
     */
    void linkLast(E e) {
        // 记录原尾部节点
        final Node<E> l = last;
        // 创建新节点
        final Node<E> newNode = new Node<>(l, e, null);
        // 尾节点指向新节点
        last = newNode;
        if (l == null)
            // 若原链表为空,则头节点也指向新节点
            first = newNode;
        else
            // 原链表尾部节点的后继节点指向新节点
            l.next = newNode;
        // 更新size
        size++;
        // 更新modCount
        modCount++;
    }

    /**
     * 在指定节点的前面插入一个节点
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 记录index节点的前驱节点
        final Node<E> pred = succ.prev;
        // 创建新节点
        final Node<E> newNode = new Node<>(pred, e, succ);
        // index节点的前驱节点指向新节点
        succ.prev = newNode;
        if (pred == null)
            // index节点的原前驱节点为null,说明原节点是头节点,则头节点指向新节点
            first = newNode;
        else
            // index节点的原前驱节点的后继节点指向新节点
            pred.next = newNode;
        // 更新size
        size++;
        // 更新modCount
        modCount++;
    }
    
    /**
     * 遍历链表找到index位置的节点
     */
    Node<E> node(int index) {
        // 若 index < size/2,就从位置0往后遍历到位置index处
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        }
        // 若 index >= size/2,就从位置size往前遍历到位置index处
        else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

删除节点

java 复制代码
    /**
     * 根据指定索引删除元素
     */
    public E remove(int index) {
        // 检查index是否越界,若是则抛出 IndexOutOfBoundsException 异常
        checkElementIndex(index);
        // 遍历链表找到指定位置节点,然后移除
        return unlink(node(index));
    }

    /**
     * 从链表中移除指定节点
     */
    E unlink(Node<E> x) {
        // assert x != null;
        // 记录待移除节点的数据、后继节点、前驱节点
        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;
            // 待移除节点的前驱节点置空,方便GC回收
            x.prev = null;
        }

        if (next == null) {
            // 后继节点为空,说明待移除节点是尾节点,则尾节点指向为待移除节点的前驱节点
            last = prev;
        } else {
            // 后继节点的前驱节点指向待移除节点的前驱节点
            next.prev = prev;
            // 待移除节点的后继节点置空,方便GC回收
            x.next = null;
        }

        // 待移除节点的元素值置空,方便GC回收
        x.item = null;
        
        // 更新size
        size--;
        // 更新modCount
        modCount++;
        // 返回移除节点的数据
        return element;
    }

    /**
     * 遍历链表找到index位置的节点
     */
    Node<E> node(int index) {
        // 若 index < size/2,就从位置0往后遍历到位置index处
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        }
        // 若 index >= size/2,就从位置size往前遍历到位置index处
        else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

查询节点

java 复制代码
    /**
     * 获取指定位置数据
     */
    public E get(int index) {
        // 检查index是否越界,若越界则抛出 IndexOutOfBoundsException 异常
        checkElementIndex(index);
        // 遍历链表找到index位置节点,返回节点中的数据
        return node(index).item;
    }

    /**
     * 遍历链表找到index位置的节点
     */
    Node<E> node(int index) {
        // 若 index < size/2,就从位置0往后遍历到位置index处
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        }
        // 若 index >= size/2,就从位置size往前遍历到位置index处
        else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

实现Deque接口的方法

实现 Deque 接口的方法,可以用作双向队列

java 复制代码
    /**
     * 队头入队
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

    /**
     * 队尾入队
     */
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }

    /**
     * 获取队头数据
     */
    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    /**
     * 获取队尾数据
     */
    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }

    /**
     * 队头出队
     */
    public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

    /**
     * 队尾出队
     */
    public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }

    /**
     * 入栈
     */
    public void push(E e) {
        addFirst(e);
    }

    /**
     * 出栈
     */
    public E pop() {
        return removeFirst();
    }

核心原理总结

  • LinkedList 实现了一个双向链表 的数据结构。链表中的每个 Node 包含三个成员变量:前驱节点( prev )、后继节点( next )和存储的元素( item )。双向链表的成员变量中存储了链表的头结点和尾节点。链表节点的增删主要就是操作节点的前驱和后继引用实现。
  • LinkedList 实现了 Deque 接口,支持在链表两端进行高效地插入和删除操作,因此也可以作为栈、队列和双端队列来使用。
  • LinkedList 插入和删除操作非常高效(尤其是两端的操作),时间复杂度为 O(1)。访问特定索引的元素效率较低,因为需要从一端开始遍历到指定位置,平均时间复杂度为 O(n)。因此,LinkedList 在处理需要频繁插入和删除操作的应用场景中表现优异,但在随机访问方面则不如基于数组的集合类型(如 ArrayList)高效。
  • 链表中是没有下标索引的,若要找到指定位置的元素,就必须要遍历链表。源码中对链表的遍历实现了优化:先将指定位置 index 与链表长度 size 的一半比较,如果 index<size/2,则从位置 0 往后遍历到位置 index 处;如果 index>size/2,则从位置 size 往前遍历到位置 index 处,从而提高遍历效率(当然,实际上遍历效率还是很低)。
相关推荐
冬夜戏雪17 分钟前
java学习 leetcode 二分查找 图论
java·学习·leetcode
专注VB编程开发20年21 分钟前
c#,vb.net全局多线程锁,可以在任意模块或类中使用,但尽量用多个锁提高效率
java·前端·数据库·c#·.net
回家路上绕了弯2 小时前
Spring AOP 详解与实战:从入门到精通
java·spring
缉毒英雄祁同伟3 小时前
企业级WEB应用服务器TOMCAT
java·前端·tomcat
青云交3 小时前
Java 大视界 -- 基于 Java 的大数据可视化在能源互联网全景展示与能源调度决策支持中的应用
java·大数据可视化·智能决策·能源互联网·三维渲染·能源调度·nsga-ii
盖世英雄酱581363 小时前
国企“高级”程序员写的那些问题代码(六期)
java·后端
藤椒鱼不爱编程3 小时前
面向对象_类与对象
java
xcnwldgxxlhtff4 小时前
Java:线程池
java·开发语言
弹简特4 小时前
【Java web】HTTP 与 Web 基础教程
java·开发语言·前端