「重学Java集合」LinkedList全面解析

1,038 阅读9分钟

Java集合:LinkedList详解


参考学习

底层实现

双!向!链!表!

ok,简单复习一下什么是链表?

链表链表听名字就是知道是一个链子状的表格,像下面这样。

image-20201013152559349

链表是一个非线性不连续的存储结构,它的数据单元最基本的可以由两部分组成,1是存放数据的空间,2是指向下一个元素的指针。这种数据结构的特点就是查找比较耗时,因为要从第一个元素开始一个个的向后遍历才可以,但是对于修改或者增加操作,相对于数组类型就比较简单快速,因为就是一个指针的变化而已。

简单说了一下链表,双向链表就是在普通链表的基础上对每个节点引入了一个指向上一个元素的指针,双向就体现在既可以向后找,也可以向前找。

image-20201013153801687

在理解了双向链表的结构之后,我们来看看LinkedList的代码实现,它是如何定义双向链表的。

LinkedList维护了一个内部类,名为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;
    }
}

然后了解下LinkedList的初始情况,通过代码可以看到,初始化情况下的LinkedList到底是个啥样子,LinkedList维护了3个成员变量,其中需要注意到的是first和last两个初始节点。

// 初始元素数量为0
transient int size = 0;
// 头结点
transient Node<E> first;
// 尾结点
transient Node<E> last;

构造器

LinkedList提供了一个空参构造函数(啥也没做的构造函数),和一个带参构造,且内容比较简单,阅读下源码:

// 0.啥也没干的空参构造函数
public LinkedList() {
}

// 1.传入一个Collection类型的带参构造函数
public LinkedList(Collection<? extends E> c) {
    this();
    // 调用addAll方法,其实学习过前面的ArrayList源码解析,看到这一步是十分亲切的
    // addAll方法见名知意,猜都能猜出来,就是创建一个长度等于c长度的链表,再把c中的元素全部插入到这个链表中
    // 具体addAll方法的细节,下面会进行详细的介绍
    addAll(c);
}

LinkedList的构造方法比较简单,不做过多的介绍。还是和ArrayList解析的节奏一样,看完了结构与构造器后面我们就详细了解下LinkedList中那些常用api。

常用API分析

首先来看看上面已经出现过的addAll方法

addAll(int index, Collection<? extends E> c)

其实LinkedList提供了两个addAll方法,其中addAll(Collection<? extends E> c)本质上调用的就是addAll(int index, Collection<? extends E> c),所以我们合并到一起学习,逐行分析

// 0. addAll方法,传入一个Collection类型的参数
public boolean addAll(Collection<? extends E> c) {
    // 其本质调用的还是下面的addAll(int index, Collection<? extends E> c)方法
    // 注意这里传入的第一个参数是size,那这句话的意思就是将collection添加到linkedlist的末尾
    return addAll(size, c);
}

// 1.核心addAll方法,两个参数,第一个指的时候需要插入的脚标,第二个代表要插入的值Collection类型
public boolean addAll(int index, Collection<? extends E> c) {
    // checkPositionIndex = 检查 位置 索引 
    // 还记得ArrayList的越界检查吗,记住有index的地方就有这个方法,防止index超出list的合理范围
    // 它的解析在下面
    checkPositionIndex(index);
	// 老朋友,又见面了,Collection.toArray方法,到处都有它的影子
    Object[] a = c.toArray();
	// 这一步,当需要添加的集合是一个空的集合,那就直接返回false,不和你多BB
    int numNew = a.length;
    if (numNew == 0)
        return false;
	// !!! 这里要注意了,出现了两个新朋友,pred和succ
    // 这两个兄弟会频繁出现在LinkedList的源码中,现在我们还不知道他的意思代表什么
    // 不过我可以剧透一下,pred代表某个元素的前一个元素,succ代表某个元素的后一个元素
    // 那么某个元素是啥呢?要晕了,不急向下看
    Node<E> pred, succ;
    if (index == size) {
	    // 当index==size,意思就是当要插入的位置是list的末尾的时候
        // 此时succ=null,pred=last(list的最后一个元素),这里可能有点点绕,举个栗子
        // 有list: 1->2->3->4->5,index=5,那么有succ指向空,pred指向5
        // 为什么要这样?其实我想你已经知道了,还记得index==size吗?
	    // 当我们要往末尾插入的时候,假设我们要插入的第一个元素就是“某个元素”
        // 那么先定义好pred就是我们要插入的元素的前一个元素,就是5这个元素,就是last
        // 而succ=null是因为我们就是往末尾插入的,末尾后面没东西了,那就是null
        // 那么这里我们也能理解上面说的“某个元素”指的就是即将要被我们操作的那个元素
        succ = null;
        pred = last;
    } else {
        // 看了上面的解释之后,我相信这里接很好理解了
        // else中的代码表示的是,当我们要往数组的某一个位置(非末尾元素)插入的时候
        // succ就是目标位置的元素,pred就是目标位置的前一个元素
        // 所以我们就可以知道 addAll执行完的结果会是下面这样的
        // 有list: 1->2->3->4->5,index=2,要插入一个A,B
        // 结果就是 1->A->B->2->3->4->5
        // 还有一个需要注意的地方,这里用了node(index),下面有详解
        succ = node(index);
        pred = succ.prev;
    }

    // 在确定好pred和succ元素后就可以开始进行正式的插入操作了
    // 遍历数组
    for (Object o : a) {
        // 新建节点,注意,新建节点的前一个节点已经指定为pred
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            // 当pred=null,就意味着当前linkedlist是一个空的list
            // 所以要插入的newNode当然是首节点啦
            first = newNode;
        else
		   // 将前一个节点的next节点与当前节点连线
            pred.next = newNode;
        // 顺推,循环
        pred = newNode;
    } // 这个循环过程如果有学习过数据结构链表相关操作的同学应该很容易就能理解
		
    // 收尾操作
    if (succ == null) {
        // 如果插入的是末尾,那么最后一个元素就我们插入的最后一个元素
        last = pred;
    } else {
		// 将剩下的线连上
        pred.next = succ;
        succ.prev = pred;
    }
	
    // 常规的size增加,表示数组容量增加
    size += numNew;
    modCount++;
    return true;
    // 看完了上面的代码还是不好理解,可以看看下面的图
}

// linked list 重写的toArray方法
public Object[] toArray() {
	// 很基础的代码,创建一个数组,然后遍历添加
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}

// index越界检查
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
		// 如果index越界就抛一个越界的异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 本质上就是判断index是不是在list的合理范围内[0,size]
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

分三个阶段:

  1. 确定pred和succ

    image-20201013170223802

  2. for循环增加节点

  3. 收尾,将新增的节点和原来的list连接

image-20201013170256858

node(int index)

在LinkedList中,node(index)这个方法在很多地方都有出现,它的作用是用于对list中的index下标的元素进行定位

Node<E> node(int index) {
    // node中做了一个小设计,由于链表的特性,导致了正常查询情况下,查询效率很低
    // 但是由于LinkedList是双向链表,支持从last元素开始向前查找,所以就可以像下面这样
    // 将list分为两段,判断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;
    }
}

add

和addAll类似,LinkedList提供了两个add方法,分别用于尾插和在任意位置插入

但是他们实质上都是调用的linkLast or linkBefore

废话不多说,直接看代码

// 0.尾插
public boolean add(E e) {
    // 实际调用的是linkLast
    linkLast(e);
    return true;
}

// 1.任意位置插入
public void add(int index, E element) {
	// 有index的地方就有越界检查
    checkPositionIndex(index);
    // index == size的情况就是插入到末尾,直接用尾插
    if (index == size)
        linkLast(element);
    else
    // 其他情况就是非末尾插入,就是在目标元素的前面插入,这点和addAll相同
        linkBefore(element, node(index));
}

// 尾插的实际操作
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    // 因为是尾插,所以last就是新节点
    last = newNode;
	// 如果是last就是null,代表是一个空list,所以first = last = newNode
    if (l == null)
        first = newNode;
    else
	// 非空list,就是常规的将前一位元素的next指向新节点
        l.next = newNode;
    // 常规:size+1
    size++;
    modCount++;
}

// 在某个节点之前插入
void linkBefore(E e, Node<E> succ) {
	// 先获取目标元素的前一个元素
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
	// 注意,既然能到这里,那么succ必然不是空元素,那么前插法,succ的前一个元素就是当前要插入的元素
    succ.prev = newNode;
    // 这里要判断下,如果被前插的元素是首位元素,那么新插入的元素就代替它成为新的首位元素
    if (pred == null)
        first = newNode;
    else
        // 否则就是将被插元素的前一个元素的next指向新元素
        pred.next = newNode;
	// 常规:size+1
    size++;
    modCount++;
}

remove

// 0.无参数的remove会直接调用removeFirst,删除首位元素
public E remove() {
    return removeFirst();
}

// 1. 指定位置删除
public E remove(int index) {
    // 有index的地方就有越界检查
    checkElementIndex(index);
    return unlink(node(index));
}

// 2.删除值等于o的首个元素
public boolean remove(Object o) {
    // 这个方法就是遍历list,然后查到值==o
    // 仅删除第一次查到的对象
    // 这里的遍历使用的方法并非node中的头尾查找,而是统一从首位开始向后查找
    if (o == null) {
        // 即使是null也可以删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                // 删除的实质方法就是unlink
                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;
}

// remove()调用此方法,删除首元素
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        // 空list调用此方法会抛出错误
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

// 有了removeFirst,同样也有removeLast,删除尾元素
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

// 删除的本质方法
E unlink(Node<E> x) {
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    // 如果删除的是首位,则首位的next元素代替成为首位
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        // x.pred指针设置为null,等待gc        
        x.prev = null;
    }

    // 如果删除的是末尾,则末尾的pred元素代替成为末尾
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        // x.next指针设置为null,等待gc
        x.next = null;
    }

    // x.item设置为null,等待gc
    x.item = null;
    // 常规:size--
    size--;
    modCount++;
    return element;
}

// 删除首位,这里默认的我们只有删除首位元素才会调用这里
// 此处逻辑比较简单,和上面的删除过程类似,不过多赘述
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null;
    first = next;
    // 空list有个单独的判断
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

// 删除末位,这里默认的我们只有删除末位元素才会调用这里
// 此处逻辑比较简单,和上面的删除过程类似,不过多赘述
private E unlinkLast(Node<E> l) {
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null;
    last = prev;
    // 空list有个单独的判断
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

set(int index, E element)

修改某个位置的值

public E set(int index, E element) {
    // 越界检查
    checkElementIndex(index);
	// 查到目标位置的node
    Node<E> x = node(index);
    // 修改并返回原值
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

get(int index)

获取某个位置的值

public E get(int index) {
    // 下面两个方法都是常见的了,不多说了
    checkElementIndex(index);
    return node(index).item;
}

indexOf(Object o)

和get反过来,这个是元素查脚标,当然咯是第一次出现的脚标

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++;
        }
    }
    // 找不到就返回-1
    return -1;
}

lastIndexOf(Object o)

和indexOf略有不同,这是查找最后一次出现的位置

public int lastIndexOf(Object o) {
    int index = size;
    // 既然是后查,那么利用双向的特点,从后往前查
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    // 找不到就返回-1
    return -1;
}

modCount

在LinkedList中它的作用和ArrayList中modCount的作用没有太大的区别,所以也不过多赘述,有疑问可以去看看之前我写的ArrayList解析

总结

以上就是对于LinkedList底层的一次解析,当然LinkedList的源码远不止这些,我只是挑出了我觉得比较重要的一些部分进行了学习梳理。

整体来看,LinkedList相较于ArrayList稍微复杂一点点,因为它是链表结构的,但是对于学习过数据结构的朋友们来说,这部分其实也很简单,很好理解,没有出现特别复杂看不懂的代码。

路漫漫其修远兮,吾将上下而求索。共勉,加油:)