2019-12-04 Java-LinkedList源码解读

@TOC

1、链表数据结构

链表分为单向链表和双向链表,他们的区别在于,单向链表只能单向寻址,而双向链表可以双向寻址。
链表中的每个单位是一个node(节点)。
双向链表:每个节点都保存 data(数据)、prev(上一个节点)、next(下一个节点)
单向链表:每个节点都保存 data(数据)、next(下一个节点)

链表数据结构的特点是:

  1. 每个节点的存储地址是不连续的 通过指针寻址找到相邻的节点。
  2. 方便节点的增删
  3. 不方便查询,需要通过指针寻址遍历节点

2、LinkedList 与ArrayList的区别

  1. LinkedList是基于链表的实现,ArrayList是基于数组实现的。
  2. LinkedList是基于链表实现的所以不需要考虑扩容的问题。
  3. LinkedList节点的增加删除效率要高于ArrayList。
  4. LinkedList的查询效率要低于ArrayList。

为什么链表 方便做节点的增删 不方便查询?

  这是一个双向列表 : A <---> B <---> C <---> D
  每个节点都存放了 其prev(上一个节点) 和 next(下一个节点) 的指针 以便确定 双向链表的顺序。

  默认情况下,如果要添加一个节点 E,则是在链表的尾部进行添加

  D元素的next 指向 E节点 ,E节点的prev指向 D
  于是整个链表变成了:  A <---> B <---> C <---> D <---> E

  如果此时要删除 C 节点
  只需要将 B节点(C节点的prev)的next 指向 D(C 节点的next)
  同时 D节点(C节点的next)的prev 指向 B(C节点的prev)
  并把 C节点的 prev 和 next 都置为空
  于是整个链表变成了: A <---> B <---> D <---> E       C(已经不会作为任何一个节点的prod 或者 next 没有引用的节点 会等待GC的回收)

从上面的例子 可以看出。

链表在删除的时候只需要修改节点之前的引用关系就行了。而数组则是需要移动被删除元素后面的所有元素的下标。增加的时候也是同理。

但是查询的时候 链表只能通过寻址来找对应的index ,而数组 则是可以通过index来确定元素所在的位置。

3、LinkedList 成员变量

LinkedList基于链表实现,其中的节点抽象成了静态内部类Node

 // 内部静态类 链表数据节点
    private static class Node<E> {
        // 数据元素
        E item;
        // 下一个节点
        LinkedList.Node<E> next;
        // 上一个节点
        LinkedList.Node<E> prev;

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

LinkedList中有几个重要的成员变量

    // 实际存储的节点数量
    transient int size = 0;

    // 头节点
    transient Node<E> first;

    // 尾节点
    transient Node<E> last;

4、 LinkedList 的基本实现(详细注解)

package com.lhit.collection;


import java.util.Collection;

public class LinkedList<E> {


///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////以下是LinkedList 基于双向链表数据结构实现的基础描述////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
/*
  这是一个双向列表 : A <-> B <-> C <-> D
  每个节点都存放了 其prev(上一个节点) 和 next(下一个节点) 的指针 以便确定 双向链表的顺序。

  默认情况下,如果要添加一个节点 E,则是在链表的尾部进行添加

  D元素的next 指向 E节点 ,E节点的prev指向 D
  于是整个链表变成了:  A <-> B <-> C <-> D <-> E

  如果此时要删除 C 节点
  只需要将 B节点(C节点的prev)的next 指向 D(C 节点的next)
  同时 D节点(C节点的next)的prev 指向 B(C节点的prev)
  并把 C节点的 prev 和 next 都置为空
  于是整个链表变成了: A <-> B <-> D <-> E    C(已经不会作为任何一个节点的prod 或者 next 没有引用的节点 会等待GC的回收)
*/

///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////以下是LinkedList中主要的成员变量和内部类//////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

    // 实际存储的节点数量
    transient int size = 0;

    // 头节点
    transient Node<E> first;

    // 尾节点
    transient Node<E> last;


    // 内部静态类 链表数据节点
    private static class Node<E> {
        // 数据元素
        E item;
        // 下一个节点
        LinkedList.Node<E> next;
        // 上一个节点
        LinkedList.Node<E> prev;

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

///////////////////////////////////////////////////////////////////////////////////////////
    ///////////LinkedList 1. 构造方法 包含 构造方法调用到的add(Collection c)方法////
//////////////////////////////////////////////////////////////////////////////////////////

    // 1. 构建一个空的List
    public LinkedList() {
    }

    // 1. 构建一个包含集合指定元素的list,并且顺序也是按照集合迭代器所返回的顺序
    public LinkedList(Collection<? extends E> c) {
        this();

        // 1.1 向list中添加集合中的所有元素
        addAll(c);
    }


    // 1.1 向list中添加集合中的所有元素
    // 追加指定集合中的全部元素到list的尾部,顺序与集合的迭代器返回的顺序一致
    // 如果在操作进行期间修改了指定的集合,则此操作的行为是不明确的,就是不知道会发生啥事情。(注意,如果指定的集合是当前列表,并且它不是空的,则会发生这种情况。)
    public boolean addAll(Collection<? extends E> c) {
        // 1.1.1 从指定index位置开始 向list中插入集合中的全部元素
        return addAll(size, c);
    }

    // 1.1.1 插入指定集合中的全部元素到当前的list中,从指定的index坐标开始。
    // 将当前位于该位置的元素(如果有的话)和随后的元素向右移动(增加它们的索引)
    // 新元素将按指定集合的迭代器返回它们的顺序 插入到列表中。
    public boolean addAll(int index, Collection<? extends E> c) {
        // 1.1.1.1 确定 index,检查坐标是否越界
        checkPositionIndex(index);

        // 1.1.1.2 把集合转成数组处理
        Object[] a = c.toArray();
        // 1.1.1.3 获取到集合的元素个数
        int numNew = a.length;
        // 1.1.1.4 如果是一个空的集合 直接返回false 不做后续操作
        if (numNew == 0)
            return false;

        // 1.1.1.5 确定 要插入的新节点 的前一个节点
        // 初始化pred-上一个节点 和 succ 需要要操作的节点
        LinkedList.Node<E> pred, succ;

        // 1.1.1.6 找到新出入元素的上一个节点 并 找到 需要操作的原来在index位置的succ节点
        // 判断要插入的坐标 是不是与当前list的实际长度相同
        if (index == size) {
            // 1.1.1.6.1 如果相同的话
            // 不需要操作节点
            // 新插入节点的上一个节点就是 原来list的 last(最后一个)节点
            succ = null;
            pred = last;
        } else {
            // 1.1.1.6.2 如果不同 就是需要操作原来在index坐标位置的节点
            // 找到当前 list 指定坐标的节点
            succ = node(index);
            // 原来在index位置的节点的 前一个节点 就是 新节点的前一个节点
            pred = succ.prev;
        }

        // 1.1.1.7 遍历数组元素 从index 位置 插入到到list中
        for (Object o : a) {
            // 1.1.1.7.1 把当前数组元素包装成 list的node节点
            @SuppressWarnings("unchecked") E e = (E) o;
            LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, null);
            // 1.1.1.7.2 确定新节点的上一个节点
            if (pred == null)
                // 1.1.1.7.2.1 如果上一步没有找到 新元素的前一个节点 那就说明原来list是一个空的list 所以 新的节点就是list的头
                first = newNode;
            else
                // 1.1.1.7.2.1 如果找到了新元素的前一个节点 那前一个节点的next(下一个节点)就是要指向新的节点
                pred.next = newNode;
            // 1.1.1.7.2 最后新节点当做 下一个元素节点的前一个节点 保证节点的顺序与数组的迭代器返回的顺序是一样的。
            pred = newNode;
        }

        // 1.1.1.8 确定 要插入的新节点 的下一个节点
        if (succ == null) {
            // 1.1.1.8.1 如果 原来list中在index位置的节点 succ 为空 说明 index后面没有要操作的元素 所以 最后一个新节点(pred) 就是 list的last节点 不用找下一个节点了
            last = pred;
        } else {
            // 1.1.1.8.2 如果 原来list中在index位置的节点 succ 不为空 那么 最后一个新的节点(pred) 的下一个节点就是 之前的succ节点 并且succ节点的前一个节点就是 最后一个新的节点(pred)
            pred.next = succ;
            succ.prev = pred;
        }

        // 1.1.1.9 list实际存储的node的数量加上 集合的元素个数
        size += numNew;
        return true;
    }

    // 1.1.1.1 / 3.1 确定 index,检查坐标是否越界
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + size;
    }

    // 1.1.1.6.2.1 找到当前 list 指定坐标的节点
    LinkedList.Node<E> node(int index) {
        // assert isElementIndex(index);
        // 这里查询过程中做了一个优化
        // 1.先判断index 是距离first 节点近 还是距离 last节点近
        // 2.如果距离first节点近 就从first节点 向后 找next节点 当 i == (index-1) 时x.next 就是index坐标对应的节点 于是找到目标节点
        // 3.如果距离last节点近 就从last节点 向前找prev节点  当 i == (index +1) 是x.prev 就是index坐标对应的节点 于是找到目标节点

        // 这样做的处是 可以减少链表查询的路径长度 。假设我们链表的总长度为20(index是 0-19).我们找 index=18的元素
        // index > (size >> 1) 所以距离 last节点近
        // 这时 只需要找到 last 的index是19 > 18 找到了x.prev 就是 目标节点了 下次循环条件不成立就跳出 返回了目标结果
        // 这种情况下 我们节省了 大部分的寻址次数。
        if (index < (size >> 1)) {
            LinkedList.Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            LinkedList.Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    // 返回LinkedList中实际存储节点的数量
    public int size() {
        return size;
    }

///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////LinkedList add方法//////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

    // 2. 添加单个节点
    // 在list的尾部 追加新节点
    public boolean add(E e) {
        // 2.1.1 追加新节点到链表的尾部
        linkLast(e);
        return true;
    }

    // 3. 添加单个节点到指定index
    // 添加节点到list的指定位置,将当前位于该位置的元素(如果有的话)和随后的任何元素向右移动(将一个元素添加到它们的索引中)。
    public void add(int index, E element) {
        // 3.1 检查数据越界
        checkPositionIndex(index);

        if (index == size)
            // 3.2 如果 index == size 其实就是在list的后面追加新的节点,就相当于是add(E e)方法
            linkLast(element);
        else
            // 3.3 在指定节点之前 添加新的元素节点
            linkBefore(element, node(index)/*这里首先搜索到指定index的节点*/);
    }


    // 2.1/3.2 追加新节点到链表的尾部
    void linkLast(E e) {
        // 2.1.1 找到 list的last节点
        final LinkedList.Node<E> l = last;
        // 2.1.2 新节点的prev节点 就是last节点 没有 next节点
        final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
        // 2.1.3 更新last节点
        last = newNode;
        if (l == null)
            // 如果last节点为null 那其实就是空的list 所以 first节点也是新节点
            first = newNode;
        else
            // 如果last节点不为null 那原来last节点额next就是新节点
            l.next = newNode;
        // 2.1.4 最后list的容量+1
        size++;
    }

    // 3.3 在非空节点succ之前插入元素e。
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 这里的succ节点 就是原list中 index坐标所对应的节点

        // 3.3.1 找到succ节点的前一个节点
        final Node<E> pred = succ.prev;
        // 3.3.2 创建新的节点
        // 新节点的pred 就是 succ节点的prev
        // 新节点next节点 就是succ节点
        final Node<E> newNode = new Node<>(pred, e, succ);

        // 3.3.3 更新节点关系
        // succ节点的前一个节点就是新节点
        succ.prev = newNode;
        if (pred == null)
            // succ的节点的前一个节点如果是null 其实就是插入到整个list的最前面 所以 first节点就是新节点
            first = newNode;
        else
            // succ的节点的前一个节点如果不是null 就需要更新pred节点的next 为新节点
            pred.next = newNode;
        // 3.3.4 最后size +1
        size++;
    }


///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////LinkedList contains  indexof 方法 判断list中是否包含某个元素////////////
//////////////////////////////////////////////////////////////////////////////////////////

    // 4. 判断list是否报案某个元素
    public boolean contains(Object o) {
        // 4.1 获取某个元素对应节点的坐标
        return indexOf(o) != -1;
    }


    // 4.1 从第0个节点开始查找 获取某个元素对应节点的坐标 所以只能找到第一个匹配的元素的index
    public int indexOf(Object o) {
        // 默认从0开始查找节点
        int index = 0;
        // 如果o是null 就开始从第0个节点找元素为null的节点 如果找到第一个为null的节点 就返回index
        if (o == null) {
            for (LinkedList.Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            // 如果o不是null 就开始从第0个节点 与对象o equals 的元素 找到第一个equals节点后返回对应的index
            for (LinkedList.Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

    // 4.2  与indexOf对应的 lastIndexOf方法 则是从最后一个节点开始查找 所以只能找到最后一个匹配的元素的index
    public int lastIndexOf(Object o) {
        // 默认最后一个节点开始查找节点
        int index = size;
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////LinkedList unlink 方法 在list中移除节点////////////
//////////////////////////////////////////////////////////////////////////////////////////

    // 5. 断开非空节点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) {
            // 如果上一个节点是null 那下一个节点就是 first节点
            first = next;
        } else {
            // 如果上一个节点不为null 那上一个节点的next 就是 移除节点的next
            prev.next = next;
            // 断开要移除节点对上一个节点的引用
            x.prev = null;
        }


        if (next == null) {
            // 如果next节点为空 那prev节点 就是last节点
            last = prev;
        } else {
            // 如果next节点不为空 那next节点的prev就是 要移除节点的prev
            next.prev = prev;
            // 断开要移除节点 与next节点的引用
            x.next = null;
        }

        // 存储元素置空
        x.item = null;
        // size -1
        size--;
        // 返回被移除的元素数据
        return element;
    }

    // 6. 断开头元素
    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--;
        return element;
    }

    // 7. 断开尾元素
    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--;
        return element;
    }


///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////LinkedList get set remove 方法////////////
//////////////////////////////////////////////////////////////////////////////////////////


    // 8. 获取指定位置的元素
    public E get(int index) {
        // 8.1 检查越界问题
        checkElementIndex(index);
        // 8.2 找到指定元素的节点,返回存储元素
        return node(index).item;
    }

    // 9. 设置指定位置的节点的存储元素
    public E set(int index, E element) {
        // 9.1 检查越界
        checkElementIndex(index);
        // 9.2 获取到指定位置的节点
        Node<E> x = node(index);
        // 9.3 获取到老的元素数据
        E oldVal = x.item;
        // 9.4 设置节点元素为新元素
        x.item = element;
        // 9.5 返回老元素数据
        return oldVal;
    }


    // 10.移除指定位置的节点
    public E remove(int index) {
        // 10.1 检查越界
        checkElementIndex(index);
        // 10.2 断开指定位置的节点链接
        return unlink(node(index));
    }

    // 10.1/9.1/8.1 检查越界
    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }


///////////////////////////////////////////////////////////////////////////////////////////
    //////////////////LinkedList clear 方法////////////
//////////////////////////////////////////////////////////////////////////////////////////


    public void clear() {
        // 遍历node节点
        for (Node<E> x = first; x != null; ) {
            Node<E> next = x.next;
            // 节点内容置空
            x.item = null;
            x.next = null;
            x.prev = null;
            // 准备处理下一个节点
            x = next;
        }
        // 头节点 尾节点 置空
        first = last = null;
        // size 置0
        size = 0;
    }


}

5、LinkedList如何优化查询的?

 LinkedList.Node<E> node(int index) {
        // assert isElementIndex(index);
        // 这里查询过程中做了一个优化
        // 1.先判断index 是距离first 节点近 还是距离 last节点近
        // 2.如果距离first节点近 就从first节点 向后 找next节点 当 i == (index-1) 时x.next 就是index坐标对应的节点 于是找到目标节点
        // 3.如果距离last节点近 就从last节点 向前找prev节点  当 i == (index +1) 是x.prev 就是index坐标对应的节点 于是找到目标节点

        // 这样做的处是 可以减少链表查询的路径长度 。假设我们链表的总长度为20(index是 0-19).我们找 index=18的元素
        // index > (size >> 1) 所以距离 last节点近
        // 这时 只需要找到 last 的index是19 > 18 找到了x.prev 就是 目标节点了 下次循环条件不成立就跳出 返回了目标结果
        // 这种情况下 我们节省了 大部分的寻址次数。
        
        if (index < (size >> 1)) {
            MyLinkedList.Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            MyLinkedList.Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容