[toc]
在對ArrayList源碼有過了解之后,現在對LinkedList源碼進行相應的分析。
1.結構及成員變量
1.1基本結構
linkedList本質是實現了一個雙向鏈表。其類繼承關系如下圖:
可以看到LinkedList繼承了AbstractSequentialList,實現了List<E>, Deque<E>, Cloneable, java.io.Serializable。比較特別的是實現了Deque雙端隊列,這是隊列基于鏈表的一種實現。
/**
* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces. Implements all optional list operations, and permits all
* elements (including {@code null}).
*
* <p>All of the operations perform as could be expected for a doubly-linked
* list. Operations that index into the list will traverse the list from
* the beginning or the end, whichever is closer to the specified index.
*
* <p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a linked list concurrently, and at least
* one of the threads modifies the list structurally, it <i>must</i> be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more elements; merely setting the value of
* an element is not a structural modification.) This is typically
* accomplished by synchronizing on some object that naturally
* encapsulates the list.
*
* If no such object exists, the list should be "wrapped" using the
* {@link Collections#synchronizedList Collections.synchronizedList}
* method. This is best done at creation time, to prevent accidental
* unsynchronized access to the list:<pre>
* List list = Collections.synchronizedList(new LinkedList(...));</pre>
*
* <p>The iterators returned by this class's {@code iterator} and
* {@code listIterator} methods are <i>fail-fast</i>: if the list is
* structurally modified at any time after the iterator is created, in
* any way except through the Iterator's own {@code remove} or
* {@code add} methods, the iterator will throw a {@link
* ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than
* risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
*
* <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw {@code ConcurrentModificationException} on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i>
*
* <p>This class is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @author Josh Bloch
* @see List
* @see ArrayList
* @since 1.2
* @param <E> the type of elements held in this collection
*/
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
}
其注釋大意為,雙向鏈表實現了List和Deque接口,并實現了所有可選的方法,可以允許元素為null。
對于一個雙向鏈表,所有操作都按照預期那樣,索引到列表的操作將從列表開始或者結束位置(以距離索引更近的位置為準)來遍歷該列表。
需要注意的是這個鏈表沒有采用synchronized實現。如果多線程并發的訪問一個鏈表,并且至少有一個線程修改了鏈表的結構,那么它必須采用同步的方式。(結構修改是指添加或者刪除一個或者多個元素的任何操作。僅僅設置元素的值并不是結構修改)。這通常是通過對自然封裝的列表對象進行同步來實現。
如果沒有這樣的對象,那么最好是用Collections.synchronizedList方法。
List list = Collections.synchronizedList(new LinkedList(...));
迭代器是fail-fast方法實現的,如果其結構被修改,那么在使用迭代的過程中將會出現ConcurrentModificationException異常。因此,在并發情況下修改,迭代器是回快速失敗。
需要注意的是,迭代器的fail-fast行為不能得到任何保證,因為一般來說,在非同步的并發修改時不可能得到任何擔保。
1.2 成員變量
transient int size = 0;
/**
* 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的成員變量主要有三個,分別是表示鏈表長度的size,以及鏈表頭和尾的指針first和last。注意這三個變量都是采用transient修飾,序列化的時候這些屬性回唄忽略。
1.3 Node
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;
}
}
這個類支持泛型,然后有兩個指針,一個next指向后一個元素,一個指針prev指向前一個元素。
2.LinkedList的數據結構及其其基本操作
2.1基本數據結構
那么我們在對代碼進行了解之后,可以發現,LinkedList實際上就是一個以Node節點為基礎的雙向鏈表,在這個鏈表中還有兩個指針first和last。假定存在一個元素為1、2、3的linkedList,其構成如下圖:
我們可以看到其內部的first和last指針分別指向這個雙向鏈表的首尾位置。然后每個node之間的連接關系也如上圖所示。
2.2 構造方法
LinkedList提供兩個構造方法,分別是:
/**
* Constructs an empty list.
*/
public LinkedList() {
}
這是一個空的構造方法,此時Linked的各個指針均為空,只是創建了一個LinkedList的對象。
當然,也可以從一個集合中產生一個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);
}
實際上這個方法還是調用之前的空構造方法,再采用addAll方法添加元素。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
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;
}
需要注意的是此時采用了一個pred指針,用來指向上一次添加的節點,這樣再加入新節點的時候就可以直接將pred設置為新增加節點的prev指針。
這個算法可以作為平時刷leetcode的時候的重要參考。
2.3 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;
}
實際上這個用得最多的add方法,實際上是調用的尾插法linkLast:
/**
* 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++;
}
引入了一個指針l,這個指針先找到last。通過Node的構造方法,將prev指向l,之后變更last為new出來的新的node節點。
此時通過l==null判斷此時list中是否為空,如果l==null則說明沒有任何元素,那么first的指針也會指向newNode。反之則將l的next指向newNode。
需要注意的是,這種修改會到modCount增加。這是實現fail-fast機制的基礎。
modCount 在其繼承的抽象類中。
此過程可以通過如下圖表示:
假定我們在linkedList中插入 1 和 2。我們來看看這個過程。
首先我們在LinkedList中添加1,由于最開始這個List為空,因此添加1的時候會導致fist和last都指向這個節點,Node上的next和prev值為空。
當我們再次添加節點2的時候,再來看這個過程。
在這個時候,l的值等于last,指向了節點1。然后new一個新的節點2,這一步的時候,node2的prev指針就指向了l。再將last指向這個新的node。此時如下所示:
再此之后再判斷,l此時不為null,那么將l的next指針指向這個新的node。
之后再把size加1,modCount加1,方法執行完成并回收局部變量。
最終如下:
上面即使LinkedList再尾部添加一個元素的過程。
2.4 add(int index, E element)
在指定索引的位置插入元素。其代碼如下:
/**
* 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));
}
實質上是對index判斷是否為size大小,如果與size一致,因為index實際上從0開始,而size從1開始計數,那么就說明應該是直接在尾部插入,直接就調用尾插法即可。反之則調用linkBefore方法。
/**
* 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++;
}
還需要注意的是此時還有個 node(index)方法。
/**
* 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;
}
}
這個方法也是一個在LinkedList中非常常用的方法。實際上是個根據index查找Node的方法。此時如果index小于size的一半,則從鏈表頭開始查找。如果大于size的一般,則從鏈表尾部開始查找。
而且這個判斷的位置采用的是位移計算>>1。這個是我們自己在寫代碼的時候需要學習的,位移計算的效率是最高的。
在linkBefore方法中,其執行過程如下圖所示。
假定我們有一個linkedList數組,其中有1、2、3共3個元素。現在需要將4插入到index為1的位置。
首先,調用node(1)方法,定義一個指針succ指向這個元素。
定義一個指針pred指向succ的prev節點。之后創建一個新節點,其prev為pred指向的節點,next為succ指向的節點。
此時將succ的prev指針指向newNode。
再進行判斷,此時pred不為null,所以將pred的next指針指向newNode。
這樣添加操作就執行完成,對size和modCount進行加1操作。之后再回收變量。操作完成之后的鏈表如下:
這樣一個linkedList的指定位置插入操作就執行完成。
2.5 get(int index)
我們再看看linked的get方法:
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
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;
}
可以看到在執行過程中首先對index進行判斷,是不是一個合法的index。index的范圍應該在0-size之間。否則返回IndexOutOfBoundsException異常。
之后再調用上文提到的node方法。
/**
* 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;
}
}
判斷index與size的一半進行對比。如果小則從前面查找,如果大則從尾部查找。
這個方法的時間復雜度是n。比較低效。
2.6 remove()
remove操作代碼如下:
/**
* 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));
}
同樣需要執行checkElementIndex。之后再執行unlink方法。
/**
* 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;
}
需要注意的是,unlink方法,中間有個node(index)方法進行查找。這樣remove也是一個耗時的過程。
unlink方法本質就是對元素的前后節點指針的修改,之后將移除的元素內部的指針也修改為null以便GC回收,最后size--,modCount再加1。
2.7 其他方法
LinkedList還有很多其他的方法。另外還實現了Deque接口,需要實現一些對隊列的操作。
- addFirst(E e) 在隊列頭部添加元素。
- addLast(E e) 在隊列尾部添加元素。
- boolean offerFirst(E e) 在隊列頭部插入元素,并返回true。
- boolean offerLast(E e) 在隊列尾部插入元素并返回true。
- removeFirst() 移除隊列頭部元素。
- removeLast(); 移除隊列尾部元素。
- E pollFirst() 取出隊列頭部元素。并在隊列中刪除。
- E pollLast() 取出隊列尾部元素。并在隊列中刪除。
- E getFirst() 得到隊列頭部元素。不改變隊列。如果隊列為空 拋出異常。
- E getLast()得到隊列尾部元素,不改變隊列。如果隊列為空則拋出異常。
- E peekFirst()得到隊列頭部元素,不改變隊列,如果為空則返回null。
- E peekLast() 得到隊列尾部元素,不改變隊列,如果為空則返回null。
- size() 得到隊列的長度。
- boolean contains(Object o) 判斷是否包含某個元素,這個效率比較低下。
- push(E e) 等價于addFirst。
- peek() 得到隊列頭部元素。
- poll() 得到隊列頭部元素并移除。
等等,還有很多方法,由于并不常用就不一一介紹了。
3 總結
本文對LinkedList的源碼進行了分析,個人認為其鏈表的操作方法,是我們在leetcode上解決鏈表問題的時候值得參考的地方。
另外,LinkedList的性能比較低下,我們對LinkedList的理解實際上是存在很多誤區的。尤其是查找的過程性能非常低。但是我們對LinkedList的remove等操作又離不開這個尋址過程。個人認為LinkedList除了可以作為隊列之外,本身并沒有太多的使用價值。
在下一篇文章中,將對LinkedList的性能進行分析。也許會改變一直以來大家的認知。