我的jdk源码(十二):LinkedList类

一、概述

    LinkedList类是基于双向链表实现的,它在内存中不占用连续的内存空间,里面的每个元素都能指向前一个元素和后一个元素,这使得它可以双向遍历。LinkedList类和ArrayList类相比,不具备快速随机访问的能力,但是插入和删除元素要比ArrayList类高效。

二、源码分析

    (1) 类的声明

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

    分析:    

        * LinkedList类继承的是AbstractSequentialList类:AbstractSequentialList类提供了一个对list的骨架型的实现,实现了一个按次序访问的功能。如果要实现随机访问,应该先使用AbstractList。也就是说LinkedList类也是不支持快速随机访问的。

        * 实现List接口,一是为了增加可读性,清晰看到实现的接口,二是降低维护成本,如果AbstractSequentialList类不实现List了,LinkedList类也不受影响。

        * 实现Deque接口,实现双端队列的一些功能方法,那么自然LinkedList类也是双端队列。

        * Cloneable接口也是克隆标记接口,表示此类可以被克隆,此类的实例可以调用clone()方法;未实现Cloneable接口的类的实例调用clone()方法会报错,在Object类中已经定义。

        * Serializable接口是序列化标记接口,表示此类可以被序列化到内存中。目的是为类可持久化,比如在网络传输或本地存储,为系统的分布和异构部署提供先决条件。

    (2) 成员变量

    //已有元素个数
    transient int size = 0;
    //头结点
    transient Node<E> first;
    //尾结点
    transient Node<E> last;
    //序列化UID
    private static final long serialVersionUID = 876323262645176354L;

    与ArrayList相比,LinkedList少了容量这个成员变量,所以理论上LinkedList是可以无限延长的,所以也不需要什么扩容之类的。transient关键字已经说过很多次了,就是标记一下,在序列化的时候不把修饰的字段进行数据持久化。

    (3) 构造函数

    //无参构造函数
    public LinkedList() {
    }
    //传入一个集合的构造函数
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    值得注意的是,当传入一个集合的时候,返回的LinkedList对象内元素的顺序是集合的迭代顺序。

    (4) 元素结构

    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存放的元素是其静态内部类Node的实例,通Node的结构我们可以清晰的看到,每个Node都会记录上一个Node和下一个Node的引用,这样就保证了LinkedList双向列表的结构。

    (5) linkFirst()方法

    //添加结点到头结点
    private void linkFirst(E e) {
        //获取当前List的头结点,取名 “老头结点”
        final Node<E> f = first;
        //组建新结点,把新结点的前结点设置为null,把新结点的后结点设置为 “老头结点”
        final Node<E> newNode = new Node<>(null, e, f);
        //把新结点设置为头结点,取名 “新头结点”
        first = newNode;
        //判断“老头结点”是否为null,也就是判读之前是否是空链表
        if (f == null)
            //如果之前就是空链表,那么新节点也是尾结点
            last = newNode;
        else
            //如果之前不是空链表,那么将“老头结点”的上一个节点指向新节点
            f.prev = newNode;
        //链表长度+1
        size++;
        //修改次数+1,为了保证多线程高并发的情况下,能够快速失败
        modCount++;
    }

    (6) linkLast()方法

    //添加新节点作为尾结点
    void linkLast(E e) {
        //获取原List的尾结点,取名“老尾结点”
        final Node<E> l = last;
        //创建新结点,把新节点的前结点设置为“老尾结点”,把新结点的后结点设置为null
        final Node<E> newNode = new Node<>(l, e, null);
        //把新节点设置为尾结点
        last = newNode;
        //判断原链表List是否为null
        if (l == null)
            //如果原List为null,那么新结点即是尾结点,也是头结点,所以这里把新结点设置为头结点
            first = newNode;
        else
            //如果原List不为null,那么就把“老尾结点”的后结点设置为新结点
            l.next = newNode;
        //元素数量+1
        size++;
        //修改次数+1
        modCount++;
    }

    (7) linkBefore()方法

    //在结点succ之前插入内容为e的节点
    void linkBefore(E e, Node<E> succ) {
        //先获取succ的前结点pred 
        final Node<E> pred = succ.prev;
        //创建新结点,新结点的前结点为succ的前结点pred,新结点的后结点为succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        //同是将新节点newNode设置为succ的前结点
        succ.prev = newNode;
        //如果succ的前结点pred为null,则表示succ是原来的头结点,所以新节点newNode成为新的头结点
        if (pred == null)
            first = newNode;
        else
            //如果succ不是头结点,那么就将pred的后结点指向新结点
            pred.next = newNode;
        size++;
        modCount++;
    }

    值得注意的是,插入元素时间复杂度是O(1),但是如果是单向链表,则因为无法获取当前结点的前结点,而导致只能通过遍历去获取,那么时间复杂度就变成了O(n)。

    (8) unlinkFirst()方法

    //移除头结点
    private E unlinkFirst(Node<E> f) {
        //获取头节点的内容element
        final E element = f.item;
        //获取头结点的后结点next
        final Node<E> next = f.next;
        //将头结点的内容置为null
        f.item = null;
        //将头结点的后结点也置为null,因为头结点的前结点本就为null,这时候其实这个结点就已经全部为null了,便于GC回收
        f.next = null; // help GC
        //把刚刚获取的节点next作为新的头结点
        first = next;
        //判断next是否为null
        if (next == null)
            //如果此时next为null,则表示此链表没有结点了,把尾结点也置为null
            last = null;
        else
            //如果此时next不为null,则表示此链表还有结点,所以把next的前结点置为null
            next.prev = null;
        //元素数量-1
        size--;
        //修改次数+1
        modCount++;
        //返回移除节点的内容
        return element;
    }

    unlinkFirst()方法是默认参数结点f就是头结点,所以源码中也没有加判断,这是因为unlinkFirst()方法和下面的unlinkLast()方法都要结合到实际的场景中使用,也就是链表的增删改查操作,里面调用的都是这些基础操作,而调用之前是做了判断的,具体我们下文再看。

    (9) unlinkLast()方法

    //移除尾结点
    private E unlinkLast(Node<E> l) {
        //获取结点l的内容element
        final E element = l.item;
        //获取l的前结点prev
        final Node<E> prev = l.prev;
        //设置l的内容为null
        l.item = null;
        //设置l的前结点为null,以便GC回收
        l.prev = null; // help GC
        //设置l的前结点prev为新的尾结点
        last = prev;
        //判断prev是否为null
        if (prev == null)
            //如果prev为null,表示链表中已无结点存在,则设置头结点也为null
            first = null;
        else
            如果prev不为null,则设置prev结点的后结点为null
            prev.next = null;
        //元素数量-1
        size--;
        //修改次数+1
        modCount++;
        //返回移除节点的内容element
        return element;
    }

    (10) unlink()方法

    //移除指定非空结点
    E unlink(Node<E> x) {
        //分别获取要移除的结点的内容、前结点、后结点
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        //如果前结点为null,表示移除的结点是头结点
        if (prev == null) {
            //将后结点next设置为新的头结点
            first = next;
        //如果移除的结点不是头结点
        } else {
            //那么设置前结点的后结点为后结点next
            prev.next = next;
            //并且设置结点x的前结点为null
            x.prev = null;
        }
        //如果后结点为null,表示移除的结点是尾结点
        if (next == null) {
            //那么将x的前结点prev设置为尾结点
            last = prev;
        } else {
            //如果后结点不为null,就将后结点的前结点设置为x的前结点prev
            next.prev = prev;
            //并且将x的后结点置为null
            x.next = null;
        }
        //此时的x结点,前结点为null,后结点为null,再将值item设置为null,整个Node对象就为null了,以便GC回收
        x.item = null;
        //结点数量-1
        size--;
        //修改次数+1
        modCount++;
        //返回移除结点x的item值element
        return element;
    }

    (11) remove()方法

    //移除一个内容为O的结点
    public boolean remove(Object o) {
        //先判断O是否为null,如果是,则遍历的时候,用==进行比较
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    //调用移除结点的方法unlink()
                    unlink(x);
                    return true;
                }
            }
        //判断O不为null,则遍历的时候,用equals进行比较
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

    实际上到这个地方,LinkedList的内部方法基本就介绍完毕, 其他的方法基本都是进行了一些判断后,调用了上面的这些方法。我们再看几个常用的普通方法源码就一目了然。

    (12) add()系列方法

    //添加一个结点e
    public boolean add(E e) {
        //直接调用linkLast()在链表末尾添加结点
        linkLast(e);
        //如果成功则返回true
        return true;
    }

    //在指定下标添加一个结点e
    public void add(int index, E element) {
        //判断是否越界,条件是index >= 0并且index <= size,源码如下
        checkPositionIndex(index);
        //如果index刚好等于size,表示是添加的尾结点,直接调用linkLast()方法
        if (index == size)
            linkLast(element);
        else
            //如果添加的结点不是尾结点,就直接调用linkBefore()方法
            linkBefore(element, node(index));
    }
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
    
    //从指定下标index开始,添加集合C中所有的元素到链表中
    public boolean addAll(int index, Collection<? extends E> c) {
        //老规矩,先判断下标是否越界
        checkPositionIndex(index);
        //将集合c转为Object数组a
        Object[] a = c.toArray();
        //得到a的长度numNew
        int numNew = a.length;
        //如果长度为0,那么表示集合c为null,直接返回false
        if (numNew == 0)
            return false;
        //声明两个Node变量pred和succ
        Node<E> pred, succ;
        //下面这个if判断,主要是为了获得插入坐标插入新Node后的前后结点
        //如果index刚好等于size,也就是说是从链表尾部开始添加,那么插入段的前结点就是原链表的尾结点last,插入段的尾结点就为null
        if (index == size) {
            succ = null;
            pred = last;
        //如果是从链表中间插入,则插入段的尾结点就是插入前原链表index位置上的结点,而插入段的前结点就是原链表Index位置上的结点的前结点
        } else {
            succ = node(index);
            pred = succ.prev;
        }
        //开始循环插入结点
        for (Object o : a) {
            //创建新结点newNode,内容为e,前结点就是我们上面记录的前结点pred,后结点是下一个新结点(如果还有的话)
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            //判断前结点pred是否为null,如果是,那么这个新结点newNode就是头结点first
            if (pred == null)
                first = newNode;
            //如果pred不为nul,那么就直接设置前结点的后结点为当前新结点newNode
            else
                pred.next = newNode;
            //将新的结点又设置为下一个节点的前结点
            pred = newNode;
        }
        //循环结束后,判断尾结点是否为null,如果尾结点succ为null,表示最后添加的结点(上一行代码可知被设置为了pred)就是整个新链表的尾结点last
        if (succ == null) {
            last = pred;
        //如果succ不为null,表示是在原链表中间插入的,那么设置记录的前结点的后结点为尾结点;设置记录的尾结点设置给后结点的前结点。上面过程中,succ在被记录后是一直没变的,pred会一直被指向最新的结点。
        } else {
            pred.next = succ;
            succ.prev = pred;
        }
        //结点数据增加数组a的长度
        size += numNew;
        //修改记录+1
        modCount++;
        //返回true表示添加成功
        return true;
    }

    以上方法中addAll(int index, Collection<? extends E> c)方法可能看代码有些晦涩,但是脑海中有整个过程模型,就很好理解了。如下图:

    (13) get()方法

    public E get(int index) {
        //检查是否越界
        checkElementIndex(index);
        //返回对应下标的Node结点的item
        return node(index).item;
    }

    Node<E> node(int index) {
        //判断坐标index是否小于1/2的size,也就是说在前半部分的话,就只用正向遍历前半段链表
        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类的实例在查找结点元素的时候,巧妙的利用了自己双向链表的特点,虽然时间复杂度还是O(n),但是也算是巨大的提升了。

    (14) clone()方法

    public Object clone() {
        LinkedList<E> clone = superClone();

        //初始化克隆后的对象clone
        clone.first = clone.last = null;
        clone.size = 0;
        clone.modCount = 0;

        //循环往clone中添加元素的引用,所以这是浅克隆
        for (Node<E> x = first; x != null; x = x.next)
            clone.add(x.item);

        return clone;
    }

    (15) toArray()系列方法

    //直接将当前的链表转化为数组
    public Object[] toArray() {
        //创建链表长度size的Object数组result
        Object[] result = new Object[size];
        int i = 0;
        //循环往result中添加元素
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

    //将当前链表的内容转化到指定数组a中
    public <T> T[] toArray(T[] a) {
        //先判断传入的数组a能否装得下整个链表,装不下则重新创建一个数组
        if (a.length < size)
            a = (T[])java.lang.reflect.Array.newInstance(
                                a.getClass().getComponentType(), size);
        int i = 0;
        Object[] result = a;
        //从坐标0开始往数组a中放入链表元素,是直接替换,而不是增加
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;

        if (a.length > size)
            a[size] = null;

        return a;
    }

    从代码中可以看出,数组a的length小于等于size时,a中所有元素被覆盖,被拓展来的空间存储的内容都是null;若数组a的length的length大于size,则0至size-1位置的内容被覆盖,size位置的元素被设置为null,size之后的元素不变。

    除了以上的方法外,LinkedList类除了Node以外,还有两个内部类分别是ListItr类和DescendingIterator类。我们再来详细看下源码:

    (16) ListItr内部类

    // 最近一次返回的节点,也是当前持有的节点
    private Entry<E> lastReturned = header;
    // 对下一个元素的引用
    private Entry<E> next;
    // 下一个节点的index
    private int nextIndex;
    private int expectedModCount = modCount;
    // 构造方法,接收一个index参数,返回一个ListItr对象
    ListItr(int index) {
        // 如果index小于0或大于size,抛出IndexOutOfBoundsException异常
        if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: "+index+
                            ", Size: "+size);
        // 判断遍历方向
        if (index < (size >> 1)) {
        // next赋值为第一个节点
        next = header.next;
        // 获取指定位置的节点
        for (nextIndex=0; nextIndex<index; nextIndex++)
            next = next.next;
        } else {
    // else中的处理和if块中的处理一致,只是遍历方向不同
        next = header;
        for (nextIndex=size; nextIndex>index; nextIndex--)
            next = next.previous;
       }
   }
    // 根据nextIndex是否等于size判断时候还有下一个节点(也可以理解为是否遍历完了LinkedList)
    public boolean hasNext() {
        return nextIndex != size;
   }
    // 获取下一个元素
    public E next() {
       checkForComodification();
        // 如果nextIndex==size,则已经遍历完链表,即没有下一个节点了(实际上是有的,因为是循环链表,任何一个节点都会有上一个和下一个节点,这里的没有下一个节点只是说所有节点都已经遍历完了)
        if (nextIndex == size)
        throw new NoSuchElementException();
        // 设置最近一次返回的节点为next节点
        lastReturned = next;
        // 将next“向后移动一位”
        next = next.next;
        // index计数加1
        nextIndex++;
        // 返回lastReturned的元素
        return lastReturned.element;
   }

    public boolean hasPrevious() {
        return nextIndex != 0;
   }
    // 返回上一个节点,和next()方法相似
    public E previous() {
        if (nextIndex == 0)
        throw new NoSuchElementException();

        lastReturned = next = next.previous;
        nextIndex--;
       checkForComodification();
        return lastReturned.element;
   }

    public int nextIndex() {
        return nextIndex;
   }

    public int previousIndex() {
        return nextIndex-1;
   }
    // 移除当前Iterator持有的节点
    public void remove() {
           checkForComodification();
            Entry<E> lastNext = lastReturned.next;
            try {
                LinkedList.this.remove(lastReturned);
            } catch (NoSuchElementException e) {
                throw new IllegalStateException();
           }
        if (next==lastReturned)
                next = lastNext;
            else
        nextIndex--;
        lastReturned = header;
        expectedModCount++;
   }
    // 修改当前节点的内容
    public void set(E e) {
        if (lastReturned == header)
        throw new IllegalStateException();
       checkForComodification();
        lastReturned.element = e;
   }
    // 在当前持有节点后面插入新节点
    public void add(E e) {
       checkForComodification();
        // 将最近一次返回节点修改为header
        lastReturned = header;
       addBefore(e, next);
        nextIndex++;
        expectedModCount++;
   }
    // 判断expectedModCount和modCount是否一致,以确保通过ListItr的修改操作正确的反映在LinkedList中
    final void checkForComodification() {
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
   }

    ListItr实现了ListIterator接口,可知它是一个迭代器,通过它可以遍历修改LinkedList。 在LinkedList中提供了获取ListItr对象的方法:listIterator(int index)。

    (17) DescendingIterator内部类

   // 获取ListItr对象
   final ListItr itr = new ListItr(size());
   // hasNext其实是调用了itr的hasPrevious方法
   public boolean hasNext() {
       return itr.hasPrevious();
   }
   // next()其实是调用了itr的previous方法
   public E next() {
       return itr.previous();
   }
   public void remove() {
       itr.remove();
   }

    这是一个反向的Iterator,也是调用的ListItr类中的方法。

三、总结

    LinkedList类是基于双向链表实现的,并且在内存中不连续,使用上没有大小限制,理论上可以无限增加,所以LinkedList类中也没有扩容方法;此外LinkedList类不具备随机访问,插入和删除效率较高,但是查找效率较低。LinkedList类允许结点的元素可以为null,也允许重复,并且也有modCount来实现快速失败的机制。双向链表由于可以反向遍历,相较于单向链表在某些操作上具有性能优势,但是由于每个结点都需要额外的内存空间来存储前驱指针,所以双向链表相对来说需要占用更多的内存空间,这也是空间换时间的一种体现。

    更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java觉浅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值