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

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

一、引言

在 Java 的世界里,数据结构是构建高效程序的基石。LinkedList 作为 Java 集合框架中一个重要的成员,以其独特的链式存储结构,在众多场景中发挥着不可替代的作用。与 ArrayList 基于数组的实现方式不同,LinkedList 采用链表结构,这使得它在插入和删除操作上具有显著的优势。本文将深入 Java LinkedList 的源码,详细剖析其内部实现机制和使用原理,带领读者领略这个数据结构的魅力。

二、LinkedList 概述

2.1 基本概念

LinkedList 是 Java 集合框架中的一个双向链表实现的列表类,它实现了 ListDeque 接口。这意味着 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:指向下一个节点的引用,如果是链表的最后一个节点,nextnull
  • prev:指向前一个节点的引用,如果是链表的第一个节点,prevnull

3.2 链表的头尾节点和大小

LinkedList 类中定义了两个重要的属性 firstlast,分别表示链表的头节点和尾节点,以及一个 size 属性表示链表中元素的数量。以下是相关源码:

java 复制代码
// 链表的头节点
transient Node<E> first;

// 链表的尾节点
transient Node<E> last;

// 链表中元素的数量
transient int size = 0;
  • first:指向链表的第一个节点,如果链表为空,firstnull
  • last:指向链表的最后一个节点,如果链表为空,lastnull
  • size:记录链表中元素的数量,初始值为 0。

3.3 构造函数

LinkedList 提供了两个构造函数,一个是无参构造函数,另一个是可以接收一个集合作为参数的构造函数。以下是构造函数的源码:

java 复制代码
// 无参构造函数,创建一个空的 LinkedList
public LinkedList() {
}

// 构造函数,接收一个集合作为参数,将集合中的元素添加到 LinkedList 中
public LinkedList(Collection<? extends E> c) {
    // 调用无参构造函数
    this();
    // 调用 addAll 方法将集合中的元素添加到链表中
    addAll(c);
}
  • 无参构造函数:创建一个空的 LinkedList,此时 firstlast 都为 nullsize 为 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. 如果 lnull,说明链表为空,将头节点也更新为 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. 如果 prednull,说明指定节点是头节点,将头节点更新为 newNode
    5. 否则,将 prednext 引用指向 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. 如果 succnull,说明是在尾部添加元素,将尾节点更新为最后一个新节点。
    7. 否则,将最后一个新节点的 next 引用指向 succsuccprev 引用指向最后一个新节点。
    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. 如果 fnull,说明链表为空,抛出 NoSuchElementException 异常。
    3. 调用 unlinkFirst 方法删除头节点。
  • unlinkFirst 方法的步骤如下:
    1. 获取头节点的元素 element
    2. 获取头节点的下一个节点 next
    3. 将头节点的元素和下一个节点引用置为 null,帮助垃圾回收。
    4. 将头节点更新为 next
    5. 如果 nextnull,说明链表只有一个节点,将尾节点也置为 null
    6. 否则,将 nextprev 引用置为 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. 如果 prevnull,说明指定节点是头节点,将头节点更新为 next
    4. 否则,将 prevnext 引用指向 next,并将指定节点的 prev 引用置为 null
    5. 如果 nextnull,说明指定节点是尾节点,将尾节点更新为 prev
    6. 否则,将 nextprev 引用指向 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. 如果要删除的元素 onull,遍历链表,找到第一个元素为 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. 如果 fnull,说明链表为空,抛出 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. 如果 lnull,说明链表为空,抛出 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 接口,通过调用 ListItrhasPrevious()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. 如果 fnull,说明队列为空,返回 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 )查找指定元素 。一旦找到匹配元素,立即返回其索引;若遍历完整个链表都未找到,则返回 -1contains(Object o) 方法根据 indexOf(o) 的返回值是否为 -1 ,判断链表是否包含指定元素 。这种实现方式虽然简单直接,但在链表长度较大时,遍历操作可能导致性能下降 。

7.2 size() 方法

size() 方法用于返回链表中元素的数量,以下是该方法的源码:

java 复制代码
// 返回链表中元素的数量
public int size() {
    return size;
}

LinkedList 内部维护了一个 size 变量,在每次添加、删除元素的操作中都会对其进行相应的增减 。因此,size() 方法直接返回 size 变量的值,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>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() 方法通过遍历链表,将每个节点的 itemnextprev 引用都置为 null ,帮助垃圾回收机制回收节点内存 。遍历完成后,将 firstlast 置为 null ,表示链表为空,并将 size 重置为 0 ,同时增加 modCount 记录结构修改 。这种方式确保了链表被彻底清空,释放了占用的内存资源 。

八、性能分析

8.1 时间复杂度分析

  • 插入操作
    • 在链表尾部插入元素(add(E e) ):由于 LinkedList 维护了尾节点引用,直接在尾部插入新节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 。
    • 在指定位置插入元素(add(int index, E element) ):需要先找到指定位置的节点,平均需要遍历链表一半的节点(node(index) 方法),时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为链表长度;找到节点后插入操作本身时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,因此整体插入操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
  • 删除操作
    • 删除头部元素(removeFirst() ):直接操作头节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 。
    • 删除指定位置元素(remove(int index) ):同样需要先找到指定位置的节点,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,找到后删除操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,整体删除操作时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
    • 删除指定元素(remove(Object o) ):需要遍历链表查找元素,最坏情况下遍历整个链表,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
  • 查找操作
    • 获取指定位置元素(get(int index) ):查找指定位置节点时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,获取元素本身时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,整体时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。
    • 判断元素是否存在(contains(Object o) ):需要遍历链表查找元素,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。

8.2 空间复杂度分析

LinkedList 每个节点存储元素以及前后节点引用,空间占用与元素数量成正比 。其空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为链表中元素的数量 。与基于数组实现的 ArrayList 相比,LinkedList 不需要预先分配固定大小的内存空间,但每个节点额外的引用会占用一定内存 。

8.3 与 ArrayList 的性能对比

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

九、总结与展望

9.1 总结

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

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

9.2 展望

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

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

相关推荐
百锦再7 分钟前
Android Studio开发中Application和Activity生命周期详解
android·java·ide·app·gradle·android studio·studio
不爱总结的麦穗14 分钟前
面试常问!Spring七种事务传播行为一文通关
后端·spring·面试
牛马baby40 分钟前
Java高频面试之并发编程-11
java·开发语言·面试
移动开发者1号1 小时前
Android现代进度条替代方案
android·app
万户猴1 小时前
【Android蓝牙开发实战-11】蓝牙BLE多连接机制全解析1
android·蓝牙
RichardLai881 小时前
[Flutter 基础] - Flutter基础组件 - Icon
android·flutter
前行的小黑炭1 小时前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
我是哪吒1 小时前
分布式微服务系统架构第124集:架构
后端·面试·github
Jenlybein1 小时前
进阶学习 Javascript ? 来看看这篇系统复习笔记 [ 面向对象篇 ]
前端·javascript·面试
清霜之辰1 小时前
安卓 Compose 相对传统 View 的优势
android·内存·性能·compose