从零开始的源码分析(LinkedList篇)

前言

上一篇讲了ArrayList的源码,今天就来学习一下同样常用的LinkedList的源码。

继承和实现

我们首先还是从LinkedList的类名开始,LinkedList继承于AbstractSequentialList
,并且实现了List接口和Deque接口,后面的Cloneable和java.io.Serializable表明这是一个可以被克隆和序列化的类。
那么List接口我们很熟悉了,那么AbstractSequentialList抽象类和Deque接口又是什么呢?

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

点进AbstractSequentialList类,我们可以看到这个类继承了抽象类AbstractList,记得之前ArrayList是直接实现了抽象类AbstractList,而LinkedList在上面多了一层。

public abstract class AbstractSequentialList<E> extends AbstractList<E>

那么这个AbstractSequentialList多出了什么内容呢?
从后面的代码中,我们可以看到AbstractSequentialList实现了一些AbstractList没有实现的或者不支持的方法,比如add、set之类的。
但是这些操作都是基于迭代器的,通过移动迭代器来进行插入和删除,这正如类中的Sequential一样是按顺序地,而非像ArrayList那样是可以随机访问的。

//这里由于长度原因就只贴出add方法了
public void add(int index, E element) {
    try {
        listIterator(index).add(element);
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

说完了AbstractSequentialList,让我们再回到LinkedList,我们发现这里还有一个Deque接口是ArrayList所不具备的。
Deque其实是我们的老朋友了,在力扣刷题的过程中我们总是会使用Deque来实现队列或者栈,从接口名中可以看出Deque继承了接口Queue,但是在后面的代码中可以看出Deque不但拥有队列的方法(poll之类的),也拥有stack的方法(pushpop),当然除此之外还有一些集合所共有的方法比如contains之类的,这里就不再展开了。

public interface Deque<E> extends Queue<E> {
	...//省略了代码
    // *** Queue methods ***
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    
    // *** Stack methods ***
    void push(E e);
    E pop();
    ...//省略了代码
}

顺便多说一句,另外两个实现了Deque接口的集合是ArrayDeque和LinkedBlockingDeque,前者从名字可以看出是由数组实现的,后者则是用链表实现的阻塞队列。

成员变量

说完了LinkedList继承和实现的接口,我们接着来看他的成员变量。
LinkedList的成员变量非常的少,只有三个。其中size代表了LinkedList的大小,而first和last指向了链表的头节点和尾节点,从transient关键字可以看出他们是不会被序列化的:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

再来看一下Node类,Node是一个静态内部类(藏得还挺下面的),使用private修饰,说明正常情况下是不可被外界访问的一个类,类中包含了前节点和后节点,这说明了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;
    }
}

LinkedList的方法大多比较相似,也没有什么特别的地方,这里就以addFirst(E e)方法来举个例子吧。
可以看到addFirst(E e)实际上只是调用了linkFirst(E e)方法而已,在这个方法中首先将新的节点插入到链表头部,然后判断一下之前的头节点是不是为空,为空的话就说明这是链表的第一个节点,所以也是最后一个节点,就把尾节点的指针也指向新的节点。
当然既然有linkFirst方法,那么还有unlinkFirst方法,代码比较相似,但是操作是相反的,这里就不再展开了。

public void addFirst(E e) {
        linkFirst(e);
    }
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++;
}

比较有意思的代码是寻找指定位置的节点,代码首先判断一下如果这个节点存在,那么是更加靠近头还是靠近尾,然后从比较近的地方开始遍历。

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        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;
    }
}

总结

最后总结一下LinkedList的特点:

  • 基于双端队列,实现了List、Deque接口。
  • 删除和添加只影响周围的两个节点,效率很高。
  • 不需要扩容
  • 由于需要存储节点的前后信息,所以占用的空间会比ArrayList更大一些。

下一篇总结一下关于map接口的两个集合,相比较list,面试一般比较喜欢问map,可能是因为map稍微难一些吧。


在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_33241802/article/details/107040854
今日推荐