之前写过一篇解读ArrayList源码的文章,现在接着来写与之关联比较近的LinkedList的源码解读,对比着学习,体会以不同方式实现的List接口的异同。
概述
简单说下LinkedList的特点:
- LinkedList使用双向链表来实现List接口和Deque接口的功能。
- 实现了所有列表有关的操作,允许添加所有类型的元素(包括null)。
- 该类是线程不安全的。
接下来开始进行源码解读,包括成员变量、构造方法、其他方法。
声明:本笔记里的源码来源于官网安装文件“jdk-8u201-windows-x64”。
成员变量(字段)
transient int size = 0;
说明:指这个链表里面存储的元素的数量。
transient Node first;
说明:指向链表里第一个结点,即表头结点。
transient Node last;
说明:指向链表里最后一个结点,即表尾结点。
private static final long serialVersionUID = 876323262645176354L;
说明:LinkedList实现了java.io.Serializable接口,这个字段是针对该接口提供的。
结点结构
凡是谈到链式结构(不论是列表、树、图,只要是链式结构实现的),首先都要定义结点,也就是一个结点里面包含什么东西,或者说如何表示一个结点。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里的一个静态内部类。
- 它有三个字段:元素本身(即数据域)、后继结点、前驱结点。
- 只有一个构造方法,在构造的时候确定所有字段,顺序是:前驱结点、数据域、后继结点。
构造方法
LinkedList共有两个构造方法,来看一下。
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* 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);
}
空参数的构造方法:
什么都没有做,那说白了就是创建一个LinkedList对象,然后里面的字段first、last都为null,size为0。
有参数的构造方法:
使用一个Collection集合来创建LinkedList,也就是将Collection里的所有元素装到LinkedList里面,这些元素在LinkedList里的顺序,由Collection的迭代器的返回顺序来决定。
这里暂时不分析有参数的构造方法的源码,因为这里涉及到添加一堆元素的操作,我们还是先分析了添加单个元素的操作,再来分析添加一堆元素的操作。
List和Deque的对比
在分析LinkedList的添加元素和移除元素的方法之前,不知道大家有没有想过,List接口和Deque接口都有添加和移除元素的方法,那么LinkedList要怎么实现它们呢?会不会冲突呢?我们接下来对比一下。
List | Deque | |
添加元素 | boolean add(E e) 和 void add(int index, E element); | boolean add(E e); |
移除元素 | boolean remove(Object o); 和 E remove(int index); | boolean remove(Object o); 和 E remove(); |
我们来看看它们的差异:
- 首先看List特有的方法,List表示列表,它支持和索引相关的操作,所以它的添加、移除元素方法里有支持传入索引的重载方法,这些是Deque所没有的。
- 接着看Deque特有的方法,Deque表示队列,比较倾向于支持对于首尾两端的操作,对于空参数的移除方法,默认移除队头的元素,这个默认操作是List所没有的。
- 接着来看有冲突的方法,对于add(E e),List要求在列表尾部插入元素,Deque要求在队列尾部插入元素,所以它们两要求的功能其实是一样的。对于remove(Object o)两者都要求移除集合里面从头至尾出现的第一个匹配元素,所以它们两要求的功能其实是一样的。
好了我们现在可以开始进入添加、移除元素以及其它方法的分析了。
public boolean add(E e)
LinkedList共有两个重载的add()方法,我们先来看一个参数的方法。
源码分析
我们把add(E e)及其内部调用的方法从头到尾列出来,全部如下:
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
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++;
}
可以看到,add(E e)并没有实际的功能,内部是用linkLast(E e)来添加元素的。
“link”表示“连接”的意思,linkLast()其实就是向链表尾部添加元素。在LinkedList里面,有若干linkXxx()方法,比如linkLast()、linkFirst()、linkBefore()分别表示向链表尾部添加元素、向链表头部添加元素、在链表的某个元素前面添加元素。这里先来看linkLast():
- 首先用一个“l”变量保存链表尾部的指针,然后创建结点,既然是在链表尾部添加结点,那该结点的前驱结点就是原来的表尾结点,后继结点是null。
- 由于新结点就是新的链表尾部结点,所以表尾字段“last”指向新的结点。
- 判断“l”是否为null,其实就是判断原来的“last”是否为null,也就是判断原来的链表是否为空链表,也就是判断此时此刻是不是在添加第一个元素。如果是在添加第一个结点,那表头表尾的指针都应该指向该新结点,所以会将表头指针指向新结点。如果不是在添加第一个结点,那就要将原来表尾元素的后继指针指向新的结点。
- 最后将表示链表尺寸的字段+1。
过程比较简单,如果对于各个指针的转变不太理解,接下来看图走一遍,就清晰了。
图示过程
下图表示的是linkLast(E e)的执行过程,如图:
以上就是向链表尾部插入元素的图示过程,左侧是向空链表尾部插入元素,右侧是向非空链表尾部插入元素。
源码小结
向链表尾部插入元素的步骤总结如下:
- 创建新结点,新结点的前驱指针指向链表尾部。
- 表尾指针指向新结点。
- 如果原来链表是空链表,则表头指针也指向新结点;否则,原来的表尾元素的后继指针指向新结点。
public void add(int index, E e)
接着来看add()的第二个重载方法:在指定的位置添加新元素。
源码分析
列出该方法及其内部调用的方法如下:
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any
* subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of a valid position for an
* iterator or an add operation.
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
* Inserts element e before non-null Node succ.
*/
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++;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
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;
}
}
有一个linkLast()由于前文已经分析过了,所以这里就不贴代码了重复了。接下来看add(int index, E e)的执行流程:
- checkPositionIndex(int index),参数检查,检查传进来的位置是否合法。如果参数不合法,就抛异常,实际的检查逻辑在isPositionIndex(int index)里面。
- boolean isPositionIndex(int index),如果参数合法,返回true,否则,返回false。合法的范围是0~size,也就是允许插入元素的最前面的位置是链表头部,最后面的位置是链表最后一个结点后面。
- 如果index等于size,相当于在链表尾部插入元素,等价于调用add(E e),所以直接就调用了linkLast(),该方法前文已经分析过了,不再重复。注意一下,如果你向空链表插入结点,等价于向表尾插入结点。如果向链表尾部以外的位置插入结点,先通过node(int index)获取指定位置的结点,然后由linkBefor(E e, Node succ)将新结点插入到index所指定的结点前面。
- Noede node(int index),根据索引获取结点。基本思路很简单,就是遍历。值得一提的小技巧是它并不总是从头到尾遍历,(size >> 1)就是折半,根据索引的位置在链表的左半部分还是右半部分选择是从头到尾遍历还是从尾到头遍历。
- linkBefore(E e, Node succ),将元素e插入到结点succ前面。定义变量“pred”保存succ的前驱结点。为元素e创建新的结点,新结点的前驱结点是succ的前驱结点,后继结点是succ。然后,让succ的前驱指针指向新结点,至此,新结点和succ的相互连接就完成了,接下来是新结点和前面元素的连接。判断原来的prev结点是否为空,如果为空,说明本次插入是在表头插入结点,也就是新结点就是新的表头结点,就让first指向新结点。如果不为空,就让原来的prev结点的后继指针指向新结点。最后将表示集合尺寸的size+1,结束。
过程并不复杂,如果对于各个指针的转变不太理解,接下来看图走一遍,就清晰了。
图示过程
注意:下图仅表示linkBefore(E e, Node succ)的过程:
以上就是向链表指定结点插入元素的图示过程,左侧是向链表头部插入结点,右侧是向链表头部以外的位置插入结点。
源码小结
向链表指定位置插入元素的步骤总结如下:
- 参数检查。
- 如果指定位置刚好是链表尾部,调用linkLast()。
- 如果指定位置不是链表尾部,调用linkBefore(),进入linkBefore():
- 创建新结点,新结点的前驱指针指向“指定位置的结点的前驱结点”,新结点的后继指针指向“指定位置的结点”。
- 指定位置的结点的前驱指针指向新结点。
- 如果指定位置的结点刚好是表头结点,将表头指针指向新结点。否则,“指定位置的结点的前驱结点的后继指针”指向新结点。
public boolean remove(Object o)
remove()共有三个重载方法,两个是来自List接口的,一个是来自Deque接口的,我们先来看来自List的remove(Object o)。
源码分析
列出该方法及其内部调用的方法如下:
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If this list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* {@code i} such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns {@code true} if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return {@code true} if this list contained the specified element
*/
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;
}
/**
* Unlinks non-null node x.
*/
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;
}
来看remove(Object o)方法:
- remove(Object o)主要做的是根据传进来的元素找到对应结点,真正移除结点的地方在unlink(Node x)里面,前面分析过linkXxx()方法,表示“连接”,就是向链表里面添加元素,那么unlink()表达了相反的意思,就是从链表里面移除元素。
- remove(Object o)查找结点的方法也很简单:从表头向表尾部遍历,具体是根据传入的对象是否为null分成两个部分。如果是null,则查找第一个数据域为null的结点,否则,查找第一个数据域等于传入参数(由equals()判定)的结点。找到之后使用unlink()移除该结点。
- E unlink(Node x),从链表里移除指定结点x。首先分别定义变量指向结点x的前驱结点和后继结点,接着开始先后处理前驱结点和后继结点。
- 如果前驱结点为空,说明移除的是表头结点,此时将表头指针指向结点x的后继结点,也就是新的表头结点。如果前驱结点不为空,则将前驱结点的后继指针指向结点x的后继结点,接着将结点x的前驱指针置为空。至此完成了对结点x的前驱结点的处理。
- 如果后继结点为空,说明移除的是表尾结点,此时将表尾指针指向结点x的前驱结点,也就是新的表尾结点。如果后继结点不为空,则将后继结点的前驱指针指向结点x的前驱结点,接着将结点x的后继指针置为空。至此完成了对结点x的后继结点的处理。
- 至此,结点x的前驱结点和后继结点(如果均有)已经连接在一起了,结点x也已经和前后两个结点断开连接。接着,释放结点x指向的元素,size字段-1,将移除的结点里的元素返回,结束。
过程并不复杂,如果对于各个指针的转变不太理解,接下来看图走一遍,就清晰了。
图示过程
注意:下图仅表示unlink(Node x)的过程:
上图分别描述了待移除结点x的四类可能情况对应的移除过程:
- 结点x既是表头也是表尾。
- 结点x是表头但不是表尾。
- 结点x不是表头但是表尾。
- 结点x既不是表头也不是表尾。
源码小结
移除链表里面指定元素的步骤总结如下:
- 找到元素所在结点,调用unlink(Node e)移除结点。
- 如果目标结点是表头结点,将表头指针指向目标结点的后继结点;否则,将目标结点的“前驱结点的后继指针”指向目标结点的后继结点,目标结点的前驱指针置空。
- 如果目标结点是表尾结点,将表尾指针指向目标结点的前驱结点;否则,将目标结点的“后继结点的前驱指针”指向目标结点的前驱结点,目标结点的后继指针置空。
- 目标结点的数据域指针置空(即释放元素),链表长度size-1。
public E remove(int index)
接下来看remove()的另一个重载方法,来自List接口的,移除指定位置的元素。
源码分析
列出该方法及其内部调用的方法如下:
/**
* Removes the element at the specified position in this list. Shifts any
* subsequent elements to the left (subtracts one from their indices).
* Returns the element that was removed from the list.
*
* @param index the index of the element to be removed
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
由于unlink(Node x)、node(int index)前文已经分析过了,所以这里就不贴代码了重复了,来看remove(int index)的执行流程:
- remove(int index)的内容很简单,首先进行参数检查,然后调用node(int index)找到指定位置的结点,然后调用unlink(Node x)进行移除。
- checkElementIndex(int index),检查索引是否正确,其内部通过isElementIndex(int index)来实现参数检查,如果参数不合法,抛异常。
- boolean isElementIndex(int index),检查参数所指向的结点是否存在,有效范围就是0 ~ (size-1)。大家看到这两个检查参数的方法里的代码段,有没觉得似曾相识呢,这里提一下,移除元素时进行参数检查的checkElementIndex(int index)、isElementIndex(int index)和前文分析添加元素时进行参数检查的checkPositionIndex(int index)、isPositionIndex(int index)代码内容几乎一模一样,唯一的区别在于添加元素时的有效范围是0 ~ size,移除元素时的有效范围是0 ~ (size - 1)。
- 找到索引对应的结点,然后移除,这两个方法前文已经分析过了,不再重复。
源码小结
由于该方法涉及的关键代码前文已经分析过了,也就不画流程图了,直接进行总结吧,从链表里移除指定位置的元素步骤的总结如下:
- 参数检查。
- 找到索引对应的结点。
- 移除结点。
public E remove()
接下来看remove()的最后一个重载方法,来自Deque接口的,移除队列头部的结点,也就是移除链表的表头结点。
源码分析
列出该方法及其内部调用的方法如下:
/**
* Retrieves and removes the head (first element) of this list.
*
* @return the head of this list
* @throws NoSuchElementException if this list is empty
* @since 1.5
*/
public E remove() {
return removeFirst();
}
/**
* Removes and returns the first element from this list.
*
* @return the first element from this list
* @throws NoSuchElementException if this list is empty
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* Unlinks non-null first node f.
*/
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;
}
我们来看移除表头结点的流程:
- remove()什么都没做,直接调用removeFirst()。
- removeFirst(),移除链表的第一个结点。首先进行参数检查,如果表头指针为null,抛异常,也就是不允许对空链表执行移除表头结点的操作。如果有表头结点,调用unlinkFirst(Node f)进行移除。
- E unlinkFirst(Node f),这是真正进行移除操作的地方。首先使用变量“next”保存表头结点的后继结点,然后,将表头结点的数据域和后继指针置空,也就是释放元素以及断开表头结点对后继结点的连接。接着判断next是否为空,其实就是判断被移除的表头结点是否同时也是表尾结点。如果是,将表尾指针置空;否则,将原表头结点的后继结点的前驱指针置空,也就是断开后继结点对表头结点的连接。
流程不算复杂,如果对于具体的移除流程不清楚,接下来看示意图吧。
图示过程
注意:下图仅表示unlinkFirst(Node f)的过程:
以上左侧表示表头结点同时也是表尾结点的情况,右侧表示表头结点只是表头结点不是表尾结点的情况。
源码小结
移除链表头部结点的remove()流程总结如下:
- 参数检查。
- 调用unlinkFirst(Node f)移除表头结点。
- 释放表头结点所持有的相关对象,表头指针指向表头结点的后继结点。如果原表头结点同时也是表尾结点,表尾指针置空;否则,原表头结点的后继结点的前驱指针置空。
public boolean addAll(Collection<? extends E> c)
前文其实已经分析完了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);
}
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the specified
* collection's iterator. The behavior of this operation is undefined if
* the specified collection is modified while the operation is in
* progress. (Note that this will occur if the specified collection is
* this list, and it's nonempty.)
*
* @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 NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* Inserts all of the elements in the specified collection into this
* list, starting at the specified position. Shifts the element
* currently at that position (if any) and any subsequent elements to
* the right (increases their indices). The new elements will appear
* in the list in the order that they are returned by the
* specified collection's iterator.
*
* @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) {
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;
}
我们来看执行流程:
- 构造方法没有实际操作,只是调用了addAll(Collection<? extends E> c)方法。
- boolean addAll(Collection<? extends E> c),将传入的集合里面所有的元素添加到链表尾部。当该方法被构造方法调用,其实就是将传入的集合里的元素添加到空链表尾部,这个方法也没有实际的操作,内部只是调用了addAll(int index, Collection<? extends E> c)方法。
- boolean addAll(int index, Collection<? extends E> c),将传入的集合里面所有的元素添加到链表里面,添加位置由index指定。说明一下,这一堆元素是被插入到原来index所指向的的结点的前面的。
- 首先进行参数检查,先对index进行检查,checkPositionIndex(int index)前文已经分析过了,不再重复。接着将传入的集合转成Object[]类型,并对其进行长度检查,如果长度为0,就不存在“添加”的行为了。
- 找到index所指向的结点及其前驱结点,“succ”指向index所指向的结点,“pred”指向其前驱结点。当(index等于size)成立时,index所指向的结点是不存在的,所以这里其实是根据index所指向的元素是否存在分开处理。同时,只有向空链表里添加元素或者在链表尾部添加元素时(index等于size)才会成立,所以此时的处理办法是“succ”只能为null,“pred”指向表尾指针last(如果链表是空链表,last为null,刚好满足)。
- 遍历Object[]里的元素,逐一为其生成新结点,从“pred”指向的结点开始,逐一连接起来。我们来看具体过程,生成新结点,新结点的前驱指针指向“pred”结点,后继指针为null。判断(pred等于null)是否成立,其实就是判断当前链表是否为空链表,只有向空链表里面添加第一个元素的时候,这个条件才会成立,此时应当修改表头指针,让first指向新结点。如果(pred等于null)不成立,则让“pred”的后继指针指向新结点,至此,新结点和“pred”结点互相连接完成。然后,让“pred”指向新结点,就可以继续添加新结点了。
- 当新元素添加完了,就要处理最后一个新结点和“succ”结点的关系了。如果(succ == null)成立,说明是在链表尾部添加元素,此时让表尾指针last指向最后一个新结点即可。否则,要让最后一个新结点和“succ”结点互相连接。
图示过程
注意:
- 下图仅表示addAll(int index, Collection<? extends E> c)的过程。
- 左上角、右上角、左下角、右下角分别描述了向空链表、在表尾、在表头、向链表中部(不是表头也不是表尾)执行addAll(int index, Collection<? extends E> c)的流程。
- 假设传进来的集合里面只有两个元素,即只需要创建两个新结点。
上图即addAll(int index, Collection<? extends E> c)的执行流程。
源码小结
addAll(Collection<? extends E> c)方法里面只是调用了addAll(int index, Collection<? extends E> c)方法,那只要对addAll(int index, Collection<? extends E> c)进行总结就可以了:
- 参数检查。
- 找到index所在的结点及其前驱结点。
- 添加所有元素。
- 判断是否为表尾部并处理。
Deque接口分析
集合的主要功能就是对数据进行“增删改查”,在LinkedList里面,“改”、“查”主要是由前文分析过的“Node node(int index)”来实现的,这里就不详细说了,那么至此,List接口里常用的“增删改查”方法在LinkedList里的具体实现基本分析完了。其实在LinkedList里面,对于“增删查”操作是提供了三类方法来实现的:
- add()、remove()、get()和element()。
- offer()、poll()、peek()。
- push()、pop()、peek()。
这里之所以没有提到“改”操作是因为它就只有一个方法:set(int index, E element),而且只在List接口里定义了,Deque接口里面没有定义“改”有关的操作。对于List里的方法add()、remove()、get()我们已经分析过了,接下来了解剩余的,对比一下Deque和List接口的差异,以及Deque里面重复定义的、功能类似的几个方法的差异。
首先是纵向对比,这三类方法的功能都是对应相同的,即add()、offer()和push()表示“增”,remove()、poll()和pop()表示“删”,get()、element()和peek()表示“查”。
接着对比第一类和第二类方法,我们先将关注点放在List接口和Deque接口的差异上来进行对比:
add()、remove()、get()主要表示对“列表(即List)”进行的操作,offer()、poll()、peek()主要表示对“队列(即Deque)”进行的操作。尽管Deque接口也定义了add()、remove()方法,但是,只有List接口里的add()、remove()方法支持传入索引作为参数(即对某个位置上的元素进行操作),Deque接口里面是完全没有任何方法支持传入索引作为参数的,这是List接口和Deque接口的主要区别。element()方法在两个接口的对比当中意义不大,下文再说。
继续对比第一类和第二类方法,但此时抛开List接口,将关注点完全集中在Deque接口自身来进行对比。实际上,第一类和第二类方法里面,除了get()以外,其余6个方法在Deque接口里面全都有定义,那它们有什么区别:
- add()、offer()均表示向链表尾部添加元素,当添加元素成功时,两者均返回true。差异在于当添加元素失败时,offer()总是返回false,如果导致失败的原因是“容量限制”,add()会抛illegalStateException异常。
- remove()、poll()均表示删除并返回链表头部的元素,差异在于如果对空链表执行删除操作,poll()会返回null,而remove()会抛NoSuchElementException异常。
- element()、peek()均表示获取(但不删除,即“查”)链表头部的元素,差异在于如果对空链表执行查找操作,peek()会返回null,element()会抛NoSuchElementException异常。
显然,就Deque接口自身而言,这两类方法的功能是一样的,只是对于异常情况的处理不同,add()、remove()、element()以抛异常来表示异常情况,offer()、poll()、peek()以特定返回值来表示异常情况。实际情况是否真的如此呢?不完全是,在LinkedList实际的实现里面,有一个方法没有按照Deque里面的声明来处理,就是add()。add()方法是没有抛异常的,至于原因,就源码的实现来看,笔者猜测在LinkedList里面add()根本就不存在失败的情况。
接着对比第二类和第三类方法,它们也是纯粹站在Deque接口的角度来看的:
offer()、poll()主要表示对“队列(即Deque)”进行的操作,push()、pop()主要表示对“栈(即Stack)”进行的操作。尽管LinkedList没有声明实现了“栈”相关的接口,但是它确实支持和“栈”有关的操作。由于数据结构对“队列”、“栈”的定义不同,因此这两类方法虽然功能对应相同,但是实现却有一个很大差异,offer()的“增”是在链表尾部进行的,poll()的“删”是在链表头部进行的,而push()和pop()的“增”、“删”都是在链表头部进行。因为数据结构对于“队列”的定义就是队尾进入,队头移出,先进先出;而对于“栈”的定义则是进出都在同一端,后进先出,这是需要注意的一点。
总结
如果想要从功能、接口层面比较好的理解LinkedList,应当要先理解好数据结构里“列表”、“队列”、“栈”的概念,因为LinkedList提供了对这三类数据结构进行操作的API。
本文分析了LinkedList所有的字段、构造方法以及部分常用的其它方法,对List和Deque接口里面定义的“增”、“删”方法在LinkedList里的具体实现进行了详细的分析,并且比较了两个接口里面“增”、“删”操作的异同。对于复杂的方法,都采用了先文字分析源码、后图片展示过程的形式,应该说是比较清晰的。从源码里可以看出,在链表里面对结点进行“增”、“删”操作时,需要特别注意处理好空链表、在表头进行操作、在表尾进行操作三类情况。
最后,对比了LinkedList里面三类“增删查”操作的异同,它们的异同主要体现在数据结构对“列表”、“队列”、“栈”三者的功能定义方面。