基本概念

在探究 LinkedList 之前首先要明白一个词:链表。

  • 概念:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
  • 结构:由一系列节点组成,节点包括值域和指针域两个部分,其中值域用来存储数据元素的值,指针域用来存储上(下)一个节点的地址。
  • 特点:链表查找和修改相应的时间复杂度分别是 O(n)、O(1)
  • 类型:链表有单链表和双链表之分,单链表只有一个指针域,指向该节点的下一个节点位置;双链表则有两个指针域,分别执向它的前节点、后节点。

原理分析

在这里主要探究的是 LinkedList 的内部构造,以及它常用的增删改查方法。

以下源码来自 JDK 1.7

1.节点元素

首先来看 LinkedList 中一个重要的静态内部类:

private static class Node<E> {
    E item; // 值域
    Node<E> next; // 后指针
    Node<E> prev; // 前指针

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

Node,也称节点。通过代码可以发现构建一个节点需要三种元素:值域、前指针、后指针,缺一不可。其中:

  • 值域:用来存放元素的值。
  • 前后指针:指针可以指向别的节点,具体作用稍后再讲。

根据描述可以绘制 Node 的结构如下:

java队列LinkedList是否为空_指定位置


2.内部结构

介绍完节点的概念,再来看看 LinkedList 的内部构造。

// 元素个数
transient int size = 0;

// 尾节点
transient Node<E> last;

// 头结点
transient Node<E> first;

// 构造函数
public LinkedList() { }

LinkedList 的三个成员变量,分别代表元素个数、尾节点、头节点,并且它们都无法被序列化。因此可以猜测其内部构造是一个链表,由多个节点链接而成。

观察它的构造函数,是一个默认构造函数。说明 LinkedList 默认创建出来后是一个空的集合,与 ArrayList 不同,它不需要指定容量,因为它的容量是可变的。

空的集合我们还看不出它具体的内部结构,因此需往集合中添加一个元素后,再作分析。

// 添加指定元素
public boolean add(E e) {
    linkLast(e);
    return true;
}


// 链接(添加)到末尾位置
void linkLast(E e) {

    final Node<E> l = last;

    // 关键-> 创建一个值域为 e 的节点,其前指针指向 l (即原来的尾节点)
    final Node<E> newNode = new Node<>(l, e, null);

    // 将新创建的节点设置为尾节点
    last = newNode;

    // 第一次往 LinkedList 中添加元素时 last 为空节点
    if (l == null){
        // 关键 ->将新创建的节点设置为首节点,说明只有一个节点时,该节点既是首节点又是尾节点
        first = newNode;
    }else{
        // 修改 l 的后指针
        l.next = newNode;
    }

    size++;
    modCount++;
}

根据代码,可以绘制 LinkedList 创建过程如下:

java队列LinkedList是否为空_集合_02

3.添加操作

这里要分析的 LinkedList 常用的添加操作有:不指定位置(添加到链表末尾)、指定位置(添加到链表指定位置)

  • 前者在上面介绍内部结构时已经分析过原理,这里不在阐述。
  • 后者与前者相比,多了查询指定位置元素的步骤。

下面来看看在指定位置添加元素的过程:

public void add(int index, E element) {
    // 校验位置是否合法
    checkPositionIndex(index);

    // 判断指定位置是否为链表末尾
    if (index == size){
        linkLast(element);
    }else{
        // 关键-> 先找到该位置的元素,再添加到该元素前面。
        linkBefore(element, node(index));
    }
}

// 校验位置是否合法
private void checkPositionIndex(int index) {
    if (!isPositionIndex(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;
    }
}

// 关键-> 这里 e 表示要添加的元素,succ 表示指定位置上的元素
void linkBefore(E e, Node<E> succ) {

    final Node<E> pred = succ.prev; 

    // ①构建新节点,节点值为 e,并指定前后节点分别为 succ 的前节点、succ
    final Node<E> newNode = new Node<>(pred, e, succ);

    // ②修改前后节点的指针
    succ.prev = newNode;
    if (pred == null){
        first = newNode;
    }else{
        pred.next = newNode;
    }
    size++;
    modCount++;
}

分析代码,在 LinkedList 的指定位置添加元素大概需要以下几个步骤:

  • 检验指定位置是否合法
  • 根据指定位置选择遍历方向,从头节点或从尾节点
  • 找到为指定位置的节点
  • 构建新节点并插入链表(关键)

整个过程最为关键的步骤在最后一步,这里绘制出整个插入过程如下(①② 对应 linkBefore 方法的注释):

java队列LinkedList是否为空_指定位置_03

4.修改操作

LinkedList 的修改操作与添加操作类似,这里不再探究,只贴出源码:

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

整个过程如下:

  • 检验指定位置是否合法
  • 找到指定位置的节点
  • 替换节点的值域(元素)

5.删除操作

这里要分析 LinkedList 常用的删除操作有三种,分别是:不指定元素的删除操作、删除指定位置的元素、删除指定元素。

  • 不指定元素的删除操作,默认是移除链表的首节点。源码如下:
public E remove() {
    return removeFirst();
}

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;

    /// 将节点的值域置空,让 GC 回收
    f.item = null;

    // 将节点的后指针置空
    f.next = null; 

    // 设置新的头节点
    first = next;

    // next 为空, 表示只有一个节点
    if (next == null){
        // 关键-> 链表只有一个节点时,该节点(f)既是头节点,也是尾节点
        // 这里要删除 f,所以要把 last 置空
        last = null;
    }else{
        // 修改后节点的前指针
        next.prev = null;
    }

    size--;
    modCount++;
    return element;
}
  • 移除指定元素
public E remove(int index) {
    checkElementIndex(index);

    // 关键 -> 从链表中移除
    return unlink(node(index));
}
  • 移除指定位置的元素,与前者相比多了查找指定位置元素的步骤:
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;
}

在上面的代码中,第二、第三种删除操作的真正执行过程都发生下 unlink 方法,其作用是将指定元素从链表中移除。下面来看它的源码:

E unlink(Node<E> x) {

    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    // prev 为空,说明整个链表只有该节点一个节点
    if (prev == null) {
        first = next;
    } else {
        // ①与前节点断开链接
        prev.next = next;
        x.prev = null;
    }

    // next 为空,说明该节点是链接的尾节点
    if (next == null) {
        last = prev;
    } else {
        // ②与后节点断开链接
        next.prev = prev;
        x.next = null;
    }

    // ③将节点的值域置空,让 GC 回收
    x.item = null;

    size--;
    modCount++;
    return element;
}

不考虑前后节点为空的情况下, 可以绘制其操作过程如下:

总的来讲,在 LinkedList 中删除操作的步骤如下:

  • 校验指定位置是否合法,并找到要操作的元素
  • 断开与前节点的链接,包括本节点的前指针、前节点的后指针
  • 断开与后节点的链接,包括本节点的后指针、后节点的前指针
  • 将节点的值域置空让 GC 生效

值得注意的是,若操作的元素为头节点或尾节点时,需要设置新的头/尾节点

6.遍历操作

一般的来说集合的遍历操作是通过迭代器(Iterator)来完成的,具体调用如下:

List<String> list = new LinkedList<String>();
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
    System.out.println(iter.next());
}

首先来看 LinkedList 的继承关系,从上到下依次是:list -> AbstractList -> AbstractSequentialList-> LinkedList。如下图所示:

java队列LinkedList是否为空_链表_04

再来看看 itertor 方法的调用过程:

// 在 List 中定义如下:
 Iterator<E> iterator();

// 在 AbstractSequentialList 中定义如下:
public Iterator<E> iterator() {
    // 该方法继承自 AbstractList 类
    return listIterator();
}

// 在 AbstractList 中定义如下:
public ListIterator<E> listIterator() {
    return listIterator(0);
}

// 在 LinkedList 中定义如下:
public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

最后来探究下遍历操作的实现过程:

// 该类是 LinkedList 的内部类
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned = null;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    // 构造函数
    ListItr(int index) {
        // nextIndex 表示开始遍历操作的位置
        // next 表示该位置的上的节点
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext()){
            // 抛出异常...
        }
        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
    final void checkForComodification() {
        if (modCount != expectedModCount){
            // 抛出异常...
        }   
    }

    //省略其他代码...
}

总结

LinkedList 内部由非循环的双向链表 构成,而链表的特点是:便于修改,不便于查询。

LinkedList 与 ArrayList 不同的是,ArrayList 内容由数组(即线性表)构成, 数组的特点是:便于查询,不便于修改。

正是由于它们的内部构造不同导致它们有了不同的特性,因此要根据不同的场景选择不同的 List。

若是频繁修改的,则选择 LinkedList 的效率更好;若是频繁查询的,则选择 ArrayList 的执行效率更好。