JAVA源码学习-LinkedList

原文链接:https://my.oschina.net/u/2610176/blog/605859

很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。

       按惯例先上LinkedList类的定义:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

      跟ArrayList继承自AbstractList不同,LinkedList继承自AbstractSequentiaList,与ArrayList一样都实现了List,Cloneable和Serializable接口,都可以克隆并序列化,不一样的是实现了Deque接口,这个接口定义了跟双端队列相关的操作。

      LinkedList通过链表实现,那么肯定以一个链表节点作为基础数据单元,跟ArrayList的数组不一样,这个链表节点的定义如下:

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之中定义了三个属性:

transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

size属性记录了LinkedList中存储的元素个数,first和last分别是LinkedList的头节点和尾节点。


LinkedList的构造方法有两个

/**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param  c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

ArrayList中的无参构造方法(默认构造函数)还默认初始化一个长度为10的数组,LinkedList中的无参构造方法啥也没有,这该如何理解?从这里看,用无参构造函数初始化的时候貌似什么也没做,其实不然,仔细看上面提到的Node类,它通过静态加载方式声明,即在工程加载时变初始化了Node类,并包含了节点,前驱,后继三个属性;在初始化LinkedList时,又将这三个属性赋给first和last,并且都为null,即得到了一个空链表。构建对象较完整的顺序是:

先在工程加载时初始化静态代码(Node类),new LinkedList()时,先执行父类的非静态代码初始化父类成员变量,然后执行父类构造方法,接着执行子类成员变量,最后才执行子类构造方法,继而完成了对象的创建。

        带参数的构造方法里有一个addAll(Collection), 即将Collection转换为LinkedList所能接受的数据(节点),将这些节点追加到linkedList中。

addAll(int, Collection),重载addAll(Collection),在指定位置插入Collection到linkedList中

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);//如果index小于0且大于size,则抛出IndexOutOfBoundsException异常。

        Object[] a = c.toArray();//将Collection转换为对象数组
        int numNew = a.length;
        if (numNew == 0)//如果待插入对象为空,则直接返回。这里为什么不直接使用a!=null?原因我的理解是执行效率问题和不容易出错
            return false;

        Node<E> pred, succ;//根据index查找对应节点信息,以确定Collection的始末位置。pred为起始位置,succ为结尾位置,可将其看作两个指针。
        if (index == size) {//如果index==size,即在链表末尾添加元素,则Collection的起始位置为链表的last,结尾位置为null
            succ = null;
            pred = last;
        } else {//否则,结尾位置为index对应的那个节点succ,起始位置为succ的前驱节点。
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {//对Collection中每一个元素创建节点newNode,使其前驱为pred,后继null
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)//如果pred==null,则说明为第一个元素,first指向newNode
                first = newNode;
            else             //否则,起始位置pred的后继节点为newNode
                pred.next = newNode;
            pred = newNode;,//变更新的起始位置为newNode
        }//该for循环执行之后会将确定好的起始位置的节点,Collection中每个元素创建的节点连接起来,此时pred存放着所有Collection添加完成后的末尾位置

        if (succ == null) {//如果结尾位置(即index对应的那个节点)为null,说明是在链表结尾添加,则将last指针指向succ的前置
            last = pred;
        } else {           //否则将所有Collection添加完成后的末尾位置pred的后继指向index对应的节点,index节点的前驱指向pred
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;//链表长度增加Collection的长度
        modCount++;
        return true;
    }

构造方法调用的addAll最终会将size作为index参数调用addAll(int index, Collection<? extends E> c),怎么实现的等看懂了以后再写。

接下来就是几个比较重要的私有方法,LinkedList的大多数公有方法里都会出现他们的身影。


linkFirst(E e),将e插入为第一个元素

/**
     * Links e as first element.
     */
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

这里我将first,last理解为C语言里的指针,将first指向的元素复制给Node f,声明一个newNode节点,使其前驱为null,节点元素为e,后继为f(此时的f为原来链表里第一个元素),此时已经完成了头结点->newNode->f的链接,在将first的指针位置更新到newNode,是newNode成为链表第一个元素。如果链表第一个元素为null,即空链表,则将last也指向newNode;否则,使f的前驱指向newNode,建立从f-》newNode的链接。同时将LinkedList的size加一。


linkLast(E e),将e插入为最后一个元素

/**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

过程跟linkFirst类似。先将LinkedList最后一个元素拷贝至l,初始化一个newNode使其前驱为l,元素为e,后继为null,此时完成从newNode->l的链接;更新last指针为newNode;如果原LinkedList最后一个节点为null(即为空链表),将first也指向newNode;否则,将原最后一个节点的后继指向newNode,完成l->newNode的链接,同时将LinkedList的size加一。


linkBefore(E e, Node<E> succ),插入一个元素到succ之前,如果succ的前置为null(即succ为第一个节点),则同linkFirst方法。

/**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

流程是:先将succ的前驱元素复制给pred,初始化newNode节点使其前驱为pred,元素为e,后继为succ,此时建立了从newNode->succ的前置元素pred,newNode到succ的链接;建立succ->newNode的链接;如果succ的前置元素为null,则令newNode为一个元素;否则令succ前置元素的后继指向newNode,建立pred->newNode的链接,同时将LinkedList的size加一。

为什么没有类似linkAfter的方法,个人理解这跟linkBefore功能上重合,ORACLE的程序猿们懒得写~JAVA源码学习-LinkedList

以上linkFirst,linkLast,linkBefore可视作LinkedList的插入方法,过程总结如下:

先找出待插入位置的元素,另存副本A;新建node节点B与A链接,并调整收尾指针first和last,最后size加1。按照这个思路在比较这三个方法,发现linkFirst对应了linkBefore的一种特殊情况,即linkBefore的succ元素的前驱为null,即succ为链表第一个元素。


接下来我们看一组unlink方法,unlink方法顾名思义就是将LinkedList中的某个元素移除,一般思路是先将这个元素的左右链接断开,然后将这个元素的前驱后继元素链接上。

unlinkFirst(Node<E> f)

/**
     * Unlinks non-null first node f.
     */
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

既然是移除linkedList的第一个元素,那么这个方法在供其他方法使用的时候,f肯定会是first指针指向的元素。

备份第一个元素f的item和后继元素,然后将f的item和后继指针指向空,即从该链表中断开链接。将first指针指向原有链表第二个元素,即f的后继。如果f的后继也为null,那说明移除后成了空链表,尾指针last也指向null,否则将f的后继元素的前驱指向null,同时size减一,返回删除节点的元素值。


unlinkLast(Node<E> l)

/**
     * Unlinks non-null last node l.
     */
    private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

类似的,unlinkLast的入参l肯定会是last指向的尾节点

备份尾节点的元素值及其前驱节点,然后将l的item和前驱指针指向空,让此节点从LinkedList中断开,将last指向l的前驱节点。如果前驱节点为空,说明移除后成了空链表,则让first也指向null,否则l前置节点的后继指向空,同时将size减一。


unlink(Node<E> x):删除LinkedList中任意非空节点x

/**
     * Unlinks non-null node x.
     */
    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;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

先备份x的元素值,前驱节点和后继节点;如果x的前驱为null(即x为链表第一个节点),则让first指向x的后继节点,否则x前驱节点的后继指向x的后继节点,并将x的前驱断开;如果x的后继节点为null(即x为链表最后一个元素),则让last指向x的前驱节点,否则x的后继节点的前驱指向x的前驱节点,并将x的后继断开;同时将x的元素值置空,size减一并返回移除的节点元素值。

从上面的插入删除方法可以看到,链表的插入删除操作是比ArrayList效率要高的。那为什么链表的查找效率比ArrayList低,那是因为ArrayList支持随机访问而链表不能,那链表是如何查找的呢?看下面的方法:

node(int index)

/**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

根据提供的元素序号index,使用二分法在链表中循环遍历对应的元素,因为链表是无序集合,只能按照每个节点的后继去挨个查找。就是说每次对LinkedList做查找,排除最好的情况,LinkedList都得或多或少把自个元素挨个点个到,这种查找方式由链表的特性决定,实属无奈之举,所以效率没ArrayList高。


理解了上面几个方法,那么下面的方法就容易多了,无非是对以上方法的封装,增加一些校验而已,这里就简写了。

getFirst(),getLast():返回链表第一个元素值和最后一个元素值,不是节点。

removeFirst(),removeLast():分别调用unlinkFirst和unlinkLast移除第一个和最后一个节点,返回节点值。

addFirst(E),addLast(E):分别调用linkFirst和linkLast增加第一个节点和最后一个节点,返回节点值。

contains(Object o):分别调用indexOf(o)对LinkedList进行遍历查找对应的元素o,没有用到node(int index)方法。

add(E):调用linkFirst第一个节点,返回true。

remove(Object):遍历LinkedList,找到Object对应的节点,使用unlink方法移除,移除成功返回true,没有该元素则返回false。

等等,分别实现由List的增删查方法,队列的增删查方法和双端队列的增删查方法,太多了,这里不一一列举了。



转载于:https://my.oschina.net/u/2610176/blog/605859

上一篇:GGR376 Regression


下一篇:LinkedList源码分析_JDK1.8.0_191