JDK底层实现源码分析系列(三) LinkedList源码分析

Author Avatar
Binux 3月 11, 2017
  • 在其它设备中阅读本文章

“合抱之木 生于毫末 九层之台 起于累土 千里之行 始于足下”

前言

JDK 版本1.7

Collection 大家族

Collection

来源Java Collections Framework

LinkedList 继承树

LinkedList

LinkedList

LinkedList从数据结构层面上说算是一个双向链表 继承于AbstractSequentialList实现了List、Deque、Cloneable、Serializable

有三个重要的成员变量first(链表的头节点)、last(链表的尾节点)、size(链表的长度)

Node为存储数据的类 共有3个变量item(元素)、next(此元素的下一个节点)、prev(此元素的上一个节点)


分析

链表和数组

  • 数组:

    数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。

  • 链表:

    链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。

  • 区别:

    数组静态分配内存,链表动态分配内存;

    数组在内存中连续,链表不连续;

    数组元素在栈区,链表元素在堆区;

    数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);

    数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

new

LinkedList一共有俩个构造方法,一个是无参的,新new一个size为空的LinkedList,另一个是传入一个Collection类型的集合使用addAll(size,c)把集合中的全部数据加入到新建的LinkedList

    /**
     * 在链表指定位置插入集合中所有数据
     *
     * @param index index at which to insert the first element
     *              from the specified collection
     * @param c collection containing elements to be added to this list
     * @return {@code true} if this list changed as a result of the call
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @throws NullPointerException if the specified collection is null
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        // index >= 0 && index <= size;
        // false throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0) // 传入集合为空
            return false;

        Node<E> pred, succ;
        // pred为被插入节点前一个节点 succ为index节点 index<size时用于移动index节点succ.prev = pred;
        if (index == size) { // 即插入链表的最后
            succ = null;
            pred = last; // 获取最后一个节点
        } else { // 插入链表中间
            succ = node(index);
            pred = succ.prev; // 获取index前一个节点
        }

        for (Object o : a) {

            E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            // 新new节点 pred指向 (index == size)(pred->last)
            // 新new节点 pred指向 (index < size)(pred->succ.prev)
            if (pred == null) // 链表为空
                first = newNode; // 新节点为头节点
            else
                pred.next = newNode; // 上次循环的节点的next设置为本次循环节点
            pred = newNode; // 设置下次循环的pred节点为本次循环增加的节点
        }

        if (succ == null) { // index == size
            last = pred; // 设置last节点为循环的最后一个节点
        } else {
            pred.next = succ; // 设置循环的最后一个节点的next为index节点
            succ.prev = pred; // 设置设置index的prev为循环的最后一个节点
        }

        size += numNew;
        modCount++; // fail-fast
        return true;
    }

插入数据

LinkedList插入数据的方法有

  • public boolean add(E e)
  • public void add(int index, E element)
  • public boolean addAll(Collection<? extends E> c)
  • public boolean addAll(int index, Collection<? extends E> c)
  • public void addFirst(E e)
  • public void addLast(E e)
  • private void linkFirst(E e)
  • void linkLast(E e)
  • void linkBefore(E e, Node succ)
  • public boolean offerFirst(E e)
  • public boolean offerLast(E e)
  • public void push(E e)
  • public E set(int index, E element)

由于添加方法太多 本篇只会讲add(int index, E element) 其他插入方法都类似

    /**
     * 在index之前插入element元素
     */
    public void add(int index, E element) {
        checkPositionIndex(index);
        // 判断索引 index >= 0 && index <= size
        if (index == size) // 即链表尾部增加
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

    /**
     * 在index之前插入element元素 (建议画图看)
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev; // 获取index节点的前一个节点
        final Node<E> newNode = new Node<>(pred, e, succ); // 新建节点 赋值 把index的prev赋给新建节点prev
        succ.prev = newNode; // 设置index节点的prev为新节点
        if (pred == null) // 即链表为空
            first = newNode; // 设置新节点为头节点
        else
            pred.next = newNode; // 设置index节点的前一个节点的下个节点为newNode
        size++;
        modCount++;
    }

删除数据

LinkedList删除数据的方法有

  • public void clear()
  • public E poll()
  • public E remove()
  • public E remove(int index)
  • public boolean remove(Object o)
  • public E removeFirst()
  • public boolean removeFirstOccurrence(Object o)
  • public boolean removeLastOccurrence(Object o)
  • public E removeLast()
  • public E pollFirst()
  • public E pollLast()
  • public E pop()
  • private E unlinkFirst(Node f)

由于方法太多 而且都类似 本篇只会选择unlinkFirst()方法分析

    /**
     * 移除first节点
     */
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item; // 获取first的元素
        final Node<E> next = f.next; // 获取first的下一个元素
        f.item = null; // 删除first的元素
        f.next = null; // 删除first指向的元素 让GC回收
        first = next; // 设置first为删除节点的下一个节点
        if (next == null) // 下个节点为空
            last = null; // 说明只有一个first节点不为null
        else
            next.prev = null; // 设置first的下个节点为null
        size--;
        modCount++;
        return element; // 返回删除数据
    }

修改数据

    /**
     * 替换指定位置的元素
     *
     */
    public E set(int index, E element) {
        checkElementIndex(index); // 检查index是否越界
        Node<E> x = node(index); // 获得指定index元素
        E oldVal = x.item; // 活动被替换的元素
        x.item = element; // 修改元素
        return oldVal; // 返回被修改的元素
    }

查询数据

LinkedList查询数据的方法有

  • public E getFirst():获取First节点元素
  • public E getLast():获取Last节点元素
  • Node node(int index):获取指定index节点元素
  • public E peek():获取First节点元素
  • public E peekFirst():获取First节点元素
  • public E peekLast():获取Last节点元素
  • public E poll():删除First节点并返回删除元素
  • public E pollFirst():删除First节点并返回删除元素
  • public E pollLast():删除Last节点并返回删除元素
  • public E pop():删除First节点并返回删除元素

由于方法太多 而且都类似 本篇只会选择get()方法分析

    /**
     * 获取指定index元素
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    /**
     * 返回指定index的节点元素 会更具index的值判断是从前找还是从后找
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) { // index < size / 2
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else { // index > size / 2
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

总结

LinkedList本质上就是一个双向链表,不会像ArrayList那样,空间不足时去扩容(array copy是一件效率非常低的操作)这样做的好处就是处理大量数据时,存储数据的时间比ArrayList要低很多。

另外LinkedList实现了Deque接口,可以通过内部,实现FIFO(先进先出)的队列或者LIFO(后进先出)的栈


著作权声明

本文首次发布于 Binux Blog,转载请保留以上链接