Java集合源码分析<二>:LinkedList的源码分析

一、底层结构原理

LinkedList底层基于双向链表实现,相比于单向链表,双向链表可以查找上个节点,实现双向遍历,更加灵活,同时占用资源也更多。

 

上图描述了LinkedList的底层结构,LinkedList内部成员变量极少,只有first、last、size,first是链表头节点,last是尾结点,size是元素数量,LinkedList链表的每个节点为Node<E>类,源码如下:

// LinkedList.java 87行

transient int size = 0; 
transient Node<E> first; // 头节点
transient Node<E> last;  // 尾节点

// LinkedList.java 970行

private static class Node<E> {
    E item;  // 元素值
    Node<E> next; // 下一个节点, 尾节点为null
    Node<E> prev; // 上一个节点, 头节点为null

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

Node中的next和prev分别指向上一个节点和下一个节点,这是链表实现的基础,后面LinkedList的增删改查都会跟随这两个变量进行。

二、添加元素

  LinkedList基于链表实现,长度跟随元素变化,所以也不会有类型ArrayList的扩容机制和长度限制,理论上在JVM内存允许范围可无限增长,LinkedList的元素添加有四个方法,add(E e),add(int index, E element),addAll(Collection<? extends E> c),addAll(int index, Collection<? extends E> c),对于指定位置的插入(add(int index, E element)方法),首先要通过遍历查找到索引index对应的节点,因为链表时间复杂度为O(N),所以LinkedList的查找是效率极低的操作,查找方法为node(int index),在node方法中,做了二分处理,使得复杂度变为O(n)/2,源码如下:

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

添加方法的最终形式可归结为三种插入方式,即头插、尾插和中间插入。

 

在头部插入时,会依据上图中头部插入的步骤,源码如下:

// LinkedList.java 125行

private void linkFirst(E e) {
    final Node<E> f = first;
    // ① 和 ②,创建新节点,上一个元素prev = null, 下一个节点指向 first
    final Node<E> newNode = new Node<>(null, e, f); 
    // ③ 头节点变为新节点
    first = newNode;
    if (f == null)
        last = newNode;
    else
        // ④ 第二个节点(原头节点)的上一个节点指向新节点
        f.prev = newNode;
    size++;
    modCount++;
}

在尾部插入,会依据图中的尾部插入的步骤, 源码如下:

// LinkedList.java 140行

void linkLast(E e) {
    final Node<E> l = last;
    // ① 和 ② , 创建新节点。 上一个节点prev = last,下一个节点next = null
    final Node<E> newNode = new Node<>(l, e, null);
    // ③ 新节点变为尾节点
    last = newNode;
    if (l == null)
        first = newNode;
    else
        // ④ 倒数第二个节点(原尾结点)的下一个节点指向新节点
        l.next = newNode;
    size++;
    modCount++;
}

中间插入源码如下:

// LinkedList.java 155行

void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev; // 下一个节点
    // ① 和 ②,创建新节点,prev = pred , next = succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // ③ 新节点与下一个节点prev关联
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        // ④ 新节点与上个节点的next关联
        pred.next = newNode;
    size++;
    modCount++;
}

三、删除元素

  LinkedList删除和添加方法相对,删除指定元素时需要遍历LinkedList查找元素,删除元素的方法(remove、poll等)同样最终归结三种,删除头节点(unlinkFirst)、删除尾节点(unlinkLast)、删除中间节点(unlink),删除时,会打断被删除节点两边的链接,然后置空value,交由GC回收对象,实现过程如下图:

 

删除头节点源码如下:

// LinkedList.java 171行

// 删除头节点
private E unlinkFirst(Node<E> f) {
        
    final E element = f.item;
    // 获取下一个节点
    final Node<E> next = f.next;
    // ① 置空头节点value和next
    f.item = null;
    f.next = null; // help GC
    // ② next变为新的头节点
    first = next;
    if (next == null)
        last = null;
    else
        // ③ 打断prev链接
        next.prev = null;
    size--;
    modCount++;
    return element;
}

删除尾结点源码如下:

// LinkedList.java 190行

private E unlinkLast(Node<E> l) {
    
    final E element = l.item;
    // 获取尾结点上一个节点
    final Node<E> prev = l.prev;
    // ① 置空头节点value和prev
    l.item = null;
    l.prev = null; // help GC
    // ② prev变为新的尾节点
    last = prev;
    if (prev == null)
        first = null;
    else
        // ③ 打断next链接
        prev.next = null;
    size--;
    modCount++;
    return element;
}

删除中间节点源码如下:

// LinkedList.java 209行

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 {
        // ① 上一个节点的next指向被删除节点next
        prev.next = next;
        // ② 断开 prev
        x.prev = null;
    }
    
    if (next == null) { //删除节点为尾结点
        last = prev;
    } else {
        // ③ 下一个节点的prev指向被删除节点的prev
        next.prev = prev;
        // ④ 断开 next
        x.next = null;
    }
    // 置空元素值
    x.item = null;
    size--;
    modCount++;
    return element;
}

 四、队列和栈功能

  LinkedList实现List接口的同时,也实现了Deque接口,Deque是双端队列,队列是先进先出的数据结构,Deque在Queue的offset和poll基础上增加了offerFirst、pollLast等方法,实现了两边都能出队和入队的操作,所以LinkedList也可以做队列来使用。同时,由于可以插入头节点和取出头节点,也可用作栈,作为队列和栈时,出入队和出入栈的操作都是基于addFirst、addLast、unLinkFirst、unLinklast四个方法实现,源码就不在分析。

 

posted @ 2019-07-11 13:14  哲雪君!  阅读(9791)  评论(0编辑  收藏  举报