LinkedList源码研究

    本笔记研究LinkedList源码。LinkedList从名字都可以看出,这是一个基于链表实现的容器,只要是基于链表实现的,那肯定都非常简单(链表跟红黑树相比,简单的掉渣);同时,由于LinkedList实现了Deque接口,所以他具有队列的一些 特性。在我看来,LinkedList就是记录了链表的头结点和尾节点,而此链表的节点有记录了前驱节点和后继节点,所以在操作链表的时候,比如插入,移除,获取元素等,既可以从表头开始,右可以从表尾开始,这就是LinkedList具有队列的部分特性的原因。好了,不啰嗦了,123,上源码:

//注意实现了Deque接口;Deque接口是Queue的子接口,我们知道Queue是
//一种队列形式,而Deque则是双向队列,它支持从两个端点方向检索和插入元素
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //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;

    /**
     * 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
     *
     * 基于一个Collection对象构造一个LinkedList
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    //LinkedList的节点类
    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;
        }
    }
}

    国际惯例,先从添加元素的方法开始分析:

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     *
     * 添加元素e
     */
    public boolean add(E e) {
        //简单的调用linkLast;同时也可以看出
        //,添加元素,默认是添加到链表的尾部的
        linkLast(e);
        return true;
    }

    add方法的核心是linkLast,下面分析:

    /**
     * Links e as last element.
     *
     * 传入一个元素e,把它放在链表尾部
     */
    void linkLast(E e) {
        //先记录尾节点
        final Node<E> l = last;

        //根据e创建一个节点newNode,第一个参数代表他的前驱节
        //点是原来的尾节点,他自身就成了尾节点;最后一个参数代表
        //他的后继节点是空;这很好理解,尾节点肯定是没有后继节点的
        final Node<E> newNode = new Node<>(l, e, null);

        //将LinkedList的尾节点指向刚才创建的节点
        last = newNode;

        //如果原来的尾节点是空,那么说明链表不存在
        //此时刚刚创建的节点不光是尾结点,还是头节点
        if (l == null)
            first = newNode;

        //如果链表存在,那么将原来的尾节点的后继节点指向刚刚创建的节点
        else
            l.next = newNode;

        //LinkedList元素个数+1
        size++;

        //modCount自增
        modCount++;
    }

    整个添加元素的过程还是蛮简单的,就是改变链表中节点的指向。
    既然有add,就有get:

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     *
     * 此方法需要传入一个索引index,代表要返回链表哪个位置的元素
     */
    public E get(int index) {
        //检查索引是否越界
        checkElementIndex(index);

        //调用node方法查找指定的节点,然后返回他的数据
        return node(index).item;
    }

    get方法的核心是node(int index),下面分析:

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);
        //类似于二分查找;如果索引index小于元素个数的一半,那么从头
        //结点开始查找,否则从尾节点开始查找,这样的判断能提高查找效率
        if (index < (size >> 1)) {

            //先保存头结点
            Node<E> x = first;

            //for循环从头结点开始往死里遍历链表
            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和get方法,再研究下其他几个方法:

    /**
     * Links e as first element.
     *
     * 传入一个元素e,把它放在链表头部
     */
    private void linkFirst(E e) {
        //把原来的头结点赋值给f
        final Node<E> f = first;

        //根据e创建一个节点newNode,第一个参数代表他没有前驱节点他
        //自身就是头结点;最后一个参数代表他的后继节点是原来的头结点
        final Node<E> newNode = new Node<>(null, e, f);

        //把刚创建的节点赋值给头结点
        first = newNode;

        //如果原来的头节点为空,说明链表不存在,此
        //时刚刚创建的节点不但是头节点,还是尾节点
        if (f == null)
            last = newNode;

        //如果链表存在,那么将原来的头结点的前驱节点指向刚刚创建的节点
        else
            f.prev = newNode;

        //LinkedList元素个数+1
        size++;

        //modCount自增
        modCount++;
    }

    /**
     * Retrieves, but does not remove, the head (first element) of this list.
     *
     * @return the head of this list, or {@code null} if this list is empty
     * @since 1.5
     *
     * 从peek这个名字就可以看出,这是类似于栈的方法,返回的是栈顶的元
     * 素;在这里返回的是链表头结点的数据,不删除节点,非常简单,不啰嗦
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    /**
     * Retrieves and removes the head (first element) of this list.
     *
     * @return the head of this list, or {@code null} if this list is empty
     * @since 1.5
     *
     * 从poll这个名字就可以看出,这是类似于队列的方法,返回的是队列队头的节点,并删除节点;
     * 这里的实现就是unlinkFirst,过程分析过,删除头结点,并返回头结点的数据,简单,不啰嗦了
     */
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

    /**
     * Inserts the specified element at the beginning of this list.
     *
     * @param e the element to add
     *
     * 将元素添加到链表头部,调用的是linkFirst(分析过)
     */
    public void addFirst(E e) {
        linkFirst(e);
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     *
     * 将元素添加到链表尾部,调用的是linkLast(分析过)
     */
    public void addLast(E e) {
        linkLast(e);
    }

    /**
     * Unlinks non-null first node f.
     *
     * 删除头节点,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;

        //LinkedList元素个数 - 1
        size--;

        //modCount自增
        modCount++;

        //返回头结点数据(第一步就保存了这个数据)
        return element;
    }

    /**
     * Unlinks non-null last node l.
     *
     * 删除尾节点,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;
    }

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

    LinkedList的本质是操作链表,非常简单,其他的方法就不啰嗦了,上一篇笔记分析了ArrayList的迭代,接下来也研究下LinkedList的迭代;ArrayList的迭代比较简单,就是把游标指向下一个数组索引,然后获取数组元素;那么LinkedList是怎么实现的呢?

private class ListItr implements ListIterator<E> {

        //最近被迭代的节点
        private Node<E> lastReturned;

        //下一个被迭代到的节点
        private Node<E> next;

        //下个节点的索引
        private int nextIndex;

        //迭代前肯定要判断modCount,防止多线程操作LinkedList
        private int expectedModCount = modCount;

        //index是创建迭代器的时候传进来的,第一次迭代的时候index是0
        ListItr(int index) {
            // assert isPositionIndex(index);
            //如果迭代的索引等于元素个数,那么下个迭代的节点就是空
            //("游标"都指向最后一个元素了,没有什么可以迭代的了)
            //否则调用node方法去获取index上的节点,这个方法分析过
            next = (index == size) ? null : node(index);

            //下个迭代节点的索引,当调用next的时候,nextIndex会自增的
            nextIndex = index;
        }

        //迭代的条件,当然是迭代的索引不能大于元素个数
        public boolean hasNext() {
            return nextIndex < size;
        }

        //真正迭代的方法
        public E next() {
            //检查modCount,防止多线程操作LinkedList
            checkForComodification();

            //如果迭代索引越界,就抛出异常
            if (!hasNext())
                throw new NoSuchElementException();

            //把next赋值给lastRetrened,最终返回
            lastReturned = next;

            //将next指向下个节点
            next = next.next;

            //迭代索引自增
            nextIndex++;

            //返回结果
            return lastReturned.item;
        }

        //迭代条件,不过这里是从尾节点开始迭代
        public boolean hasPrevious() {
            return nextIndex > 0;
        }

        //之前分析过,LinkedList具有队列的部分特性,既可以
        //从头结点操作他也可以从尾节点操作他,上面的迭代过程
        //是从头结点开始迭代,这里是从尾节点开始迭代LinkedList
        public E previous() {
            //检查modCount,防止多线程操作LinkedList
            checkForComodification();

            //如果迭代到了头结点,那么应该停住,否则就抛出异常给你尝尝
            if (!hasPrevious())
                throw new NoSuchElementException();

            //将next赋值给lastReturned;如果next是null,说明是第一次迭代
            //这时候就把尾节点赋值给next,否者就把next的前驱节点赋值给next
            lastReturned = next = (next == null) ? last : next.prev;

            //索引自减,如果从尾节点开始迭代,那么整个索引的初值是链表长度
            nextIndex--;

            //返回节点数据
            return lastReturned.item;
        }
    }

    LinkedList基本上就是这么多内容了吧,要想理解带有Linked类型的集合,链表肯定是需要理解的。
    最近两篇笔记研究了ArrayList和LinkedList的源码,从源码可以看出,他们底层的数据存储方式是不一样的:
    ArrayList采用数组的方式存储数据,数组的查找、更新效率很高,因为数组保存了首元素的地址,传入一个索引,就可以根据 首地址 + 元素占用空间*索引 的方式找到该数据;而插入和删除效率就比较低了,因为数组元素之间的地址是连续的,一旦删除某个元素,为了保证这种连续性,需要把被删除元素后面的元素往前移动,这种移动是个耗时操作;数组 的插入过程也比较耗时,因为可能涉及到扩容,扩容也是需要操作整个数组的内存的。
    LinkedList采用链式方式存储数据。采用这种方式存储数据,查找、更新效率较低(相对于数组),因为需要遍历链表;但是插入和删除相对于数组来说比较高效,他只需要遍历链表,无需移动元素。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容