前言
上一篇讲了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的方法(push
、pop
),当然除此之外还有一些集合所共有的方法比如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稍微难一些吧。