LinkedList源码解析

LinkedList源码解析

源码基于java8

LinkedList整体结构

在这里插入图片描述

LinkedList是实现了List接口和Deque接口,他的结构类似于双端链表。
实现Cloneable接口表示节点可以被浅拷贝,实现了Serializable接口代表可被序列化。
LinkedList是线程不安全的,如果想变成线程安全的可以使用Collections中的
synchronizedList方法。

我们可以先看一下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,Node都有pre和next属性。


//默认链表大小是0
transient int size = 0;

    /**
     * 指向第一个Node
     * first必须满足他的prev是null并且他自身不是null
     *
     */
    transient Node<E> first;

    /**
        last的next是null,并且自身不是null
     */
    transient Node<E> last;

    /**
     * 构造方法
     */
    public LinkedList() {
    }

当链表中没有数据时,first和last是同一个节点,前后都指向null。

所以可以结构图可以是这样(自己画的比较丑,别介意)
在这里插入图片描述
ps: 在Java8中已经不是循环链表了,只是双向链表。

新增

追加节点时,可以从头部追加也可以新增到链表尾部,默认是追加到链表尾部,add方法是追加尾部,addFirst是从头部开始追加。

先看add方法


public boolean add(E e) {
        linkLast(e);
        return true;
    }
//追加在链表最后
void linkLast(E e) {
//last是最后一个节点
        final Node<E> l = last;
        //因为追加到链表最后所以新节点的next=null
        final Node<E> newNode = new Node<>(l, e, null);
        //用新节点替换掉旧的last节点
        last = newNode;
        //旧的last是null,代表是第一次添加,所以新节点就是first
        if (l == null)
            first = newNode;
        else
        //否则,last.next=newNode
            l.next = newNode;
        //最后链表长度自增1,修改版本数加1
        size++;
        modCount++;
    }

从头部追加(addFirst方法)

//在链表头部增加
public void addFirst(E e) {
        linkFirst(e);
    }
//连接新元素,新元素作为first
private void linkFirst(E e) {
//首选记录下,上次的first
        final Node<E> f = first;
        //新建一个节点,因为追加在链表头,所以前驱是null,后继是上次的first
        final Node<E> newNode = new Node<>(null, e, f);
        //然后新节点作为现在的first
        first = newNode;
        //如果是第一次增加在链头,那么就是last节点
        if (f == null)
            last = newNode;
        else
        //否则旧的first的前驱节点就是当前新增的节点
            f.prev = newNode;
        //链表长度++,版本++
        size++;
        modCount++;
    }


删除

链表的节点删除和新增方式类似,可从尾部删除也可以从头部删除,删除会把节点的值前后节点都设置成null,方便GC回收。

根据节点值删除


 public boolean remove(Object o) {
 //删除的节点是null
        if (o == null) {
        //从头结点开始循环遍历,直到遇到第一个节点值是null的,删除
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                //删除并返回true
                    unlink(x);
                    return true;
                }
            }
        } else {
        //如果要删除的元素不是null,还是需要一次遍历节点
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                //通过equals方法判断是不是将要删除的节点
                    unlink(x);
                    return true;
                }
            }
        }
        //如果将要删除的节点不在链表中会返回false
        return false;
    }

unlink方法,删除的具体操作


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;
            //然后将被删除的节点的前驱置为null,有助于GC更快回收该对象
            x.prev = null;
        }
        //删除后继节点,如果后继节点是null,那代表是最后一个节点
        if (next == null) {
        //所以删除后,最后一个节点就是被删除节点的前驱
            last = prev;
        } else {
        //如果被删除的节点的后继不是null,那么后继节点的前驱就是被删除节点的前驱
            next.prev = prev;
            //最后需要将被删除的节点的后继指针指向null,也是帮助GC
            x.next = null;
        }
        //最后将被删除节点设置为null,链表长度--,版本号++。
        x.item = null;
        size--;
        modCount++;
        //返回待删除的元素
        return element;
    }

删除指定位置的节点


public E remove(int index) {
        //因为根据下标删除,所以需要检查一下是否发生越界
        checkElementIndex(index);
        return unlink(node(index));
    }
    
    //如果下标在这0~size就返回true,否则就会抛出 IndexOutOfBoundsException
    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }
    //检查下标是否越界
    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

删除链表头部的节点


//pop调用removeFirst,removeFirst调用unlinkFirst方法
public E pop() {
        return removeFirst();
    }

public E removeFirst() {
        final Node<E> f = first;
        //如果first不存在就不能删除,直接抛出NoSuchElementException异常
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

//具体删除还得看unlinkFirst方法
private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //记录要删除的节点最终需要返回
        final E element = f.item;
        //first的后继节点
        final Node<E> next = f.next;
       //将被删除的节点的值置为null
       f.item = null;
       //这一步帮助GC
        f.next = null; 
        //然后被删除的节点的后继节点现在就成了,first节点
        first = next;
        //如果此时他为null,那就是个空链表
        if (next == null)
            last = null;
        else
        //否则的话,next的前驱需要指向null,因为first的前驱就是null,他将要变成first
            next.prev = null;
        
        //最终链表长度-1,版本号+1
        size--;
        modCount++;
        return element;
    }

查询

LinkedList查询节点的速度是比较慢的,需要挨个循环查找。

根据链表索引位置查询节点

//根据元素下标,返回非空节点
Node<E> node(int index) {
        // assert isElementIndex(index);
        //如果index处于队列的牵绊部分,从头开始查找,size>>1=size/2
        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;
            //循环到index的后一个节点
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            //返回node
            return x;
        }
    }

在查找的时候,LinkedList并没有采用从头到尾进行循环的方法,而是根据二分查找的思想,缩小了查找范围。如果在链表的前半部分就从前开始查,如果在后半部分流从后往前查,这样做提高了一些性能。

因为LinkedList也实现了Deque接口,而Deque是继承自Queue接口的。
所以实现了Queue的一些方法。

这里进行简单的对比:

  • 新增 add offer;二者底层实现相同。
  • 删除 remove poll(e) 链表为空remove会抛出NoSuchElementException,poll会返回null
  • 查找 element peek 链表为null,element会抛出NoSuchElementException异常,peek返回null。

总结

ArrayList和LinkedList的对比

  1. 两者底层实现的数据结构不同。ArrayList底层是动态数组实现,LinkedList底层是双向链表。
  2. ArrayList和LinkedList都是不同步的,也就是不能保证线程安全。如果有线程安全问题,会抛出CouncurrentModificationException的错误,意思是在当前环境中,数组合链表的结构已经被其它线程所修改。所以 换成CopyOnWriteArrayList并发集合类使用或者Collection#synchronized。
  3. LinkedList不支持随机元素访问,ArrayList支持。因为ArrayList实现了RandomAccess接口,这个接口只是一个标识接口,接口中什么都没定义,相当于空接口,在binarySearch方法中,要判断传入的List集合是否是这个接口的实现,如果实现了RandomAccess接口才能使用indexedBinarySearch方法,否则只能一个个顺序遍历,也就是调用iteratorBinarySearch方法。
  4. 空间占用上LinkedList每次操作的对象就是Node节点,这个Node中有前驱和后继,而ArrayList的空间消耗主要是他在数组尾部会预留一定的空余,所以LinkedList的空间消耗比ArrayList更多。
  5. 再来看新增和删除。ArrayList顺序插入到数组尾部,时间复杂度是O(1),如果是指定位置插入或删除的话,时间复杂度是O(n-i);i是插入/删除的位置,n代表长度。LinkedList插入和删除操作的是节点的前驱和后继,所以直接改变指向,时间复杂度都是O(1)。LinkedList在做新增和删除的时候,慢在寻找被删除的元素,快在改变前后节点的引用地址。而ArrayList在新增和删除的时候慢在数组的copy,快在寻找被删除/新增的元素。

以上有本人理解不到位的地方,欢迎各位指出,共同学习,共同进步!

posted @ 2020-05-09 10:18  起个名字都这么男  阅读(72)  评论(0编辑  收藏  举报