LinkedList底层源码阅读

前言

LinkedList区别于ArrayList主要在于它的底层是一个双向链表而不是一个数组,因为里面的方法并没有使用synchronized修饰,所以是线程不安全的。该数据结构增删快,查询慢。因此比较适合用迭代器进行遍历,而不推荐使用普通for循环。

类体系

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

从中可见LinkedList继承了AbstractSequentialList,百度百科对其解释为:
"public abstract class AbstractSequentialListextends AbstractList此类提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。"说白了就是让访问链表的效率得到提升的一个抽象类;

List:线性表的顶层接口,无需赘述。

Deque:双向队列,用来实现双向链表而存在;

Cloneable:用来支持clone(),标志性接口;

Serializable:可序列化的接口;

成员变量

transient int size = 0;

用来记录元素的容量,transient表明序列化的时候是不参与的;

/**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

链表的头指针,不序列化;
Invariant的英文释义为“不变的,恒定的”,此处可理解为定理:除非头指针和尾指针都为null,否则头指针的前一个节点一定为null,但是头指针的值不能为null。这个理解不了的话自己去看一下数据结构基础,不再赘述。

/**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

链表的尾指针,不参与序列化;

Node为静态内部类,来看看其构造:

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

静态内部类的属性其实非常接近普通的顶层类了,可以脱离顶层类而存在,其中可以定义静态方法,具体类容可以参见  点击此处
此类就是一个常见的节点类,E表示当前存储的类实例,next表示指向的下一节点,prev则表示上一节点;

构造函数

/**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

代码解读:
构造一个空的list,没什么好讲的;

/**
     * 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
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

构造一个通过集合的迭代器返回的排过序的包含所有指定元素的数组;


为了阅读体验我不复制注释了,但会解释一下;


public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

代码解读:
这个方法调用了另一个addAll;如果在该方法执行期间集合别修改那么该行为就是未定义的(这种情况回发生在指定的集合是list且不为空的时候);调用addAll(size,c);就是在index为size的地方插入集合c;如果返回的是boolean说明没有添加,如果是true则表明增加了元素;

public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;//要插入位置的原节点,下面遍历完后的后继节点
            pred = last;//前驱节点
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

代码解读:

  1. 首先检查index的合法性,之后就是把集合转为数组,获取数组的长度,如果是0直接return false,因为一个空数组根本没什么好添加的。
  2. 声明两个Node节点pred,succ,其中pred是前驱节点,succ是要插入位置的原节点,当index == size时,说明此时访问的是末尾节点,即last的next,所以其前驱节点是原链表的last,后继节点是null(此时还未插入);而当index!=size的时候,说明还没到结尾,当前节点可以用node(int index)方法去获得,然后它的前驱节点就是succ的前一个节点;
  3. 遍历数组a,创建一个以pred为前驱,e为data的节点,如果pred为null,说明要插入的位置是头结点,则链表的first=newNode,则把新节点插入到头节点的位置了,如果不为null的话则只需让pred的下一个节点是newNode即可,最后则是把pred设置为newNode,因为一遍遍历过后要把指针后移;
  4. 遍历完成之后就是把succ节点重新放到新的链表之后,如果succ为null的话,那么此时的pred即是最后一个节点,即last=pred;如果succ不为null的话则pred.next=succ,succ.prev=pred,相当于一个双向链表互相指向对方,我画图不太好,就不用画图来展现思维了,如果太抽象无法想象,是可以通过画图来展现的;
  5. size就是LinkedList实力存储的对象的个数,所以此处在原基础上加上了新加数组的长度,modCount++表明了做了一次修改。

再来看看node(int 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 {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

其实就是当index小于size/2的时候就让其由头到index-1进行遍历,以此来获得第index个数,如果index>=size/2则是进行从尾到index+1的遍历

API解读

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

代码解读:
该方法就是把以e为data的一个节点插入到头结点的方法,linkFirst解释就是把头节点链接起来的意思嘛;
使f指向first节点,新创建一个头结点,前驱节点为null,data为e,f作为其后继节点;然后头节点就是newNode了;如果f是null的话则让last节点也是newNode,因为只有一个节点,所以头结点和尾节点都是同一个节点,如果不为null的话,f的前驱节点就是newNode;

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

代码解读:
光是看名字就知道和上面的方法是一头一尾,插入尾元素;

void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

程序解读
断言目标位置原节点succ不是null的情况下,前驱就是succ的前驱,而succ成了插入节点的后继,之后的都是异曲同工,不再赘述;

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--;
        modCount++;
        return element;
    }

代码解读:
unlinkFirst就是把头结点进行解链;
断言f是原头节点且不为null,取出f的item(即data)以及其后继节点,让next节点去指向它,这样就可以设置item和next为null了,以便GC将其回收,这样next就是头节点了,如果next指向的是个null,那么就是全空的情况,尾节点也是null了,否则就是next的前驱节点为null了,因为它已经是头指针了;size–,因为少了一个元素。

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

代码解读:
unlinkLast(Node l):就是和unlinkFirst差不多的,不再赘述;

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

代码解读:
unlink(Node x)很明显就是对x进行解链,首先就是x不能为null,其他其实说白了一般情况下就是把x的前驱节点的后继引用指向x的后继节点,把x的后继节点的前驱引用指向x的前驱节点;如果x为first,那么就是它的后继节点为first;反之last就是前驱变last,若只有x则都为null;

public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

代码解读:
获取头节点的实例;

public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }

代码解读:
获取尾节点的实例;

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

代码解读:
移除头节点,当头节点为null的时候报错,否则就执行unlinkFirst(f);该段代码已在上面解读过了;

public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }

代码解读:
同上;

public void addFirst(E e) {
        linkFirst(e);
    }

代码解读:
详见likeFirst(e);后面的addLast(E e)也是一个道理,不再单独写了;

public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

代码解读:
又是判断是否包含o,其实对o的判断与ArrayList如出一辙,就是遍历的方式不同,一个是数组的搜索,一个是链表的搜索,具体来看indexOf(o)的实现:

public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

代码解读:
当o为null的时候,从头节点遍历;找到item为null的就返回;下一个则是用equals()去比较元素,很简单;

public int size() {
        return size;
    }

代码解读:
返回size()的值,没什么特别的;

public boolean add(E e) {
        linkLast(e);
        return true;
    }

代码解读:
其实链表的操作真的挺简单的,add就是末尾再加个元素就好,无需多说

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

代码解读:
删除操作,其实还是和上面的差不多,遍历链表,找到指定的第一个元素就执行unlink(x);然后返回true,否则返回false;

public void clear() {
        // Clearing all of the links between nodes is "unnecessary", but:
        // - helps a generational GC if the discarded nodes inhabit
        //   more than one generation
        // - is sure to free memory even if there is a reachable Iterator
        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;
        modCount++;
    }

代码解读:
从头节点遍历,便利时先取出下一个节点,然后清空当前节点,再让原本当前节点的引用指向已经取出来的下一个节点。当所有的操作都完成后,再让first和last所指向的也为null;将size清零。

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

代码解读:
这是一个位置访问操作,检查index的合法性,然后获取index节点处的item值;

public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

代码解读:
用指定的元素取代一个指定位置的list的元素。先检查index的合法性;之后获得指定index的node,获取该node的item设置为oldVal用于返回,将x的item设置为element,将原来的oldVal返回;

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

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

代码解读:
在指定的index处插入element,如果index==size就把element插入到末端,若不等于,则把element插入在node(index)之前;

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

代码解读:
将指定index进行解链操作;

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

代码解读:
判断该index是否是已经存在的元素的索引;

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

代码解读:
判断该索引是否是一个迭代器或者增加操作的合法位置;

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

代码解读:
主要用于输出抛出IndexOutOfBoundsException时的message;

private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

代码解读:
上述两个方法的延伸,不再赘述;

public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

代码解读:
peek就是偷看一眼的意思,其实就是返回头节点,但是并不删除(注意和pop弹栈的区别);

public E element() {
        return getFirst();
    }

代码解读:也是返回头节点,和上述方法及其相似,但是当节点为null的时候就会抛出NoSuchElementException,这是唯一的不同;


看了一下其他API都差不多,感觉有兴趣的完全可以自己去阅读一下,我就不一一解读了。

发布了37 篇原创文章 · 获赞 10 · 访问量 751

猜你喜欢

转载自blog.csdn.net/weixin_41746577/article/details/103052814