Java源码解析之LinkedList

前言

上篇文章Java源码解析之ArrayList我们分析了ArrayList的jdk源码实现,本文来看一下它的兄弟LinkedList的源码。

原理

和ArrayList底层使用数组不同,LinkedList底层使用双向链表实现,这直接决定了LinkedList随机插入、删除效率很高,不需要像数组那样拷贝和移位,但随机查找的效率就比较低了,因为需要通过遍历链表来定位元素。

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;
        }
    }
复制代码

同时维护了两个首尾指针first和last

    /**
     * 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;
复制代码

构建出来的双向链表如下图

LinkedList数据结构.jpg

下面我们就通过图文结合的方式来解析其中几个重要方法,看图很好理解各个链表操作的效果

add(index, element)方法

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
复制代码

当index==size时,直接在链表尾部添加元素,执行linkLast(element);方法

    void linkLast(E e) {
        //将l指针指向尾节点
        final Node<E> l = last;
        //构建新节点
        final Node<E> newNode = new Node<>(l, e, null);
        //将last指针指向新节点
        last = newNode;
        if (l == null)
            first = newNode;
        else
        //将原尾节点的next指针指向新节点
            l.next = newNode;
        size++;
        modCount++;
    }
复制代码

final Node<E> newNode = new Node<>(l, e, null);这行代码会在链表尾部构建一个新节点newNode,此时整个链表如下:

LinkedList数据结构 (1).jpg

l.next = newNode;修改l节点的next指针,l节点也就是原尾节点,此时原尾节点的next指针会指向我们新添加的节点,这样就将新节点挂到了链表的尾部,效果如下:

LinkedList数据结构 (2).jpg

我们再来看下,如果新节点不是添加在尾部,而是加在中间是如何实现的呢?

当index!=size时,会在index位置处插入节点,执行的是linkLast(element);方法

    //succ入参传进来的index位置的节点node(index),后面称之为succ节点
    void linkBefore(E e, Node<E> succ) {
        // 将pred指针指向index节点的前节点
        final Node<E> pred = succ.prev;
        //构建一个新节点,prev指针指向pred,next指针指向succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        //将succ节点的prev指针指向新节点
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
复制代码

此时,我们假设传进来的index是1,也就是在第二个节点前添加新节点

final Node<E> newNode = new Node<>(pred, e, succ);
构建完新节点之后的效果如下:

LinkedList数据结构 (4).jpg

succ.prev = newNode;将succ节点的prev指针指向新节点

pred.next = newNode;将pred指向的节点的next指针指向新节点

走完上面两行代码之后,就完成了新节点的添加,将newNode节点插入到了index所在的位置,整个链表如下图:

LinkedList数据结构 (5).jpg

get(index)方法

讲完了插入方法,我们再来看如何从链表中获取元素,下面是get方法

   public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
复制代码

一共就两行代码,第一行检查元素越界,不多说了,第二行是重点,通过node方法来定位节点

    Node<E> node(int index) {
        // assert isElementIndex(index);
        //判断索引位置是否小于size的一半,size>>1等效size/2
        if (index < (size >> 1)) {
        //如果索引小于size的一半,从头部开始遍历
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
        //如果索引小于size的一半,从尾部开始遍历
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
复制代码

上面的代码很通俗易懂,就是通过Node的next方法往后遍历,或者通过prev方法往前遍历,这里的一个亮点在于判断了index是否超过了链表长度的一半,从而决定是往前还是往后遍历,最多只需遍历半条链表,提高效率。

以上,我们通过分析add方法和get方法,知道了LinkedList是如何通过链表来添加元素和获取元素的,实现过程并不是很复杂,同理,对于删除元素、更新元素等其他方法,我们只要通过画图的方式就很容易搞懂每一步的操作,此处强烈建议对于链表操作可以画图进行理解,如果光看代码可能不是很好理解,一画图边一目了然

好了,今天的文章就是这些内容,如果有不同看法或疑问欢迎随时和我讨论

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享