一、概述
LinkedList類是基於雙向鏈表實現的,它在內存中不佔用連續的內存空間,裏面的每個元素都能指向前一個元素和後一個元素,這使得它可以雙向遍歷。LinkedList類和ArrayList類相比,不具備快速隨機訪問的能力,但是插入和刪除元素要比ArrayList類高效。
二、源碼分析
(1) 類的聲明
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
分析:
* LinkedList類繼承的是AbstractSequentialList類:AbstractSequentialList類提供了一個對list的骨架型的實現,實現了一個按次序訪問的功能。如果要實現隨機訪問,應該先使用AbstractList。也就是說LinkedList類也是不支持快速隨機訪問的。
* 實現List接口,一是爲了增加可讀性,清晰看到實現的接口,二是降低維護成本,如果AbstractSequentialList類不實現List了,LinkedList類也不受影響。
* 實現Deque接口,實現雙端隊列的一些功能方法,那麼自然LinkedList類也是雙端隊列。
* Cloneable接口也是克隆標記接口,表示此類可以被克隆,此類的實例可以調用clone()方法;未實現Cloneable接口的類的實例調用clone()方法會報錯,在Object類中已經定義。
* Serializable接口是序列化標記接口,表示此類可以被序列化到內存中。目的是爲類可持久化,比如在網絡傳輸或本地存儲,爲系統的分佈和異構部署提供先決條件。
(2) 成員變量
//已有元素個數
transient int size = 0;
//頭結點
transient Node<E> first;
//尾結點
transient Node<E> last;
//序列化UID
private static final long serialVersionUID = 876323262645176354L;
與ArrayList相比,LinkedList少了容量這個成員變量,所以理論上LinkedList是可以無限延長的,所以也不需要什麼擴容之類的。transient關鍵字已經說過很多次了,就是標記一下,在序列化的時候不把修飾的字段進行數據持久化。
(3) 構造函數
//無參構造函數
public LinkedList() {
}
//傳入一個集合的構造函數
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
值得注意的是,當傳入一個集合的時候,返回的LinkedList對象內元素的順序是集合的迭代順序。
(4) 元素結構
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存放的元素是其靜態內部類Node的實例,通Node的結構我們可以清晰的看到,每個Node都會記錄上一個Node和下一個Node的引用,這樣就保證了LinkedList雙向列表的結構。
(5) linkFirst()方法
//添加結點到頭結點
private void linkFirst(E e) {
//獲取當前List的頭結點,取名 “老頭結點”
final Node<E> f = first;
//組建新結點,把新結點的前結點設置爲null,把新結點的後結點設置爲 “老頭結點”
final Node<E> newNode = new Node<>(null, e, f);
//把新結點設置爲頭結點,取名 “新頭結點”
first = newNode;
//判斷“老頭結點”是否爲null,也就是判讀之前是否是空鏈表
if (f == null)
//如果之前就是空鏈表,那麼新節點也是尾結點
last = newNode;
else
//如果之前不是空鏈表,那麼將“老頭結點”的上一個節點指向新節點
f.prev = newNode;
//鏈表長度+1
size++;
//修改次數+1,爲了保證多線程高併發的情況下,能夠快速失敗
modCount++;
}
(6) linkLast()方法
//添加新節點作爲尾結點
void linkLast(E e) {
//獲取原List的尾結點,取名“老尾結點”
final Node<E> l = last;
//創建新結點,把新節點的前結點設置爲“老尾結點”,把新結點的後結點設置爲null
final Node<E> newNode = new Node<>(l, e, null);
//把新節點設置爲尾結點
last = newNode;
//判斷原鏈表List是否爲null
if (l == null)
//如果原List爲null,那麼新結點即是尾結點,也是頭結點,所以這裏把新結點設置爲頭結點
first = newNode;
else
//如果原List不爲null,那麼就把“老尾結點”的後結點設置爲新結點
l.next = newNode;
//元素數量+1
size++;
//修改次數+1
modCount++;
}
(7) linkBefore()方法
//在結點succ之前插入內容爲e的節點
void linkBefore(E e, Node<E> succ) {
//先獲取succ的前結點pred
final Node<E> pred = succ.prev;
//創建新結點,新結點的前結點爲succ的前結點pred,新結點的後結點爲succ
final Node<E> newNode = new Node<>(pred, e, succ);
//同是將新節點newNode設置爲succ的前結點
succ.prev = newNode;
//如果succ的前結點pred爲null,則表示succ是原來的頭結點,所以新節點newNode成爲新的頭結點
if (pred == null)
first = newNode;
else
//如果succ不是頭結點,那麼就將pred的後結點指向新結點
pred.next = newNode;
size++;
modCount++;
}
值得注意的是,插入元素時間複雜度是O(1),但是如果是單向鏈表,則因爲無法獲取當前結點的前結點,而導致只能通過遍歷去獲取,那麼時間複雜度就變成了O(n)。
(8) unlinkFirst()方法
//移除頭結點
private E unlinkFirst(Node<E> f) {
//獲取頭節點的內容element
final E element = f.item;
//獲取頭結點的後結點next
final Node<E> next = f.next;
//將頭結點的內容置爲null
f.item = null;
//將頭結點的後結點也置爲null,因爲頭結點的前結點本就爲null,這時候其實這個結點就已經全部爲null了,便於GC回收
f.next = null; // help GC
//把剛剛獲取的節點next作爲新的頭結點
first = next;
//判斷next是否爲null
if (next == null)
//如果此時next爲null,則表示此鏈表沒有結點了,把尾結點也置爲null
last = null;
else
//如果此時next不爲null,則表示此鏈表還有結點,所以把next的前結點置爲null
next.prev = null;
//元素數量-1
size--;
//修改次數+1
modCount++;
//返回移除節點的內容
return element;
}
unlinkFirst()方法是默認參數結點f就是頭結點,所以源碼中也沒有加判斷,這是因爲unlinkFirst()方法和下面的unlinkLast()方法都要結合到實際的場景中使用,也就是鏈表的增刪改查操作,裏面調用的都是這些基礎操作,而調用之前是做了判斷的,具體我們下文再看。
(9) unlinkLast()方法
//移除尾結點
private E unlinkLast(Node<E> l) {
//獲取結點l的內容element
final E element = l.item;
//獲取l的前結點prev
final Node<E> prev = l.prev;
//設置l的內容爲null
l.item = null;
//設置l的前結點爲null,以便GC回收
l.prev = null; // help GC
//設置l的前結點prev爲新的尾結點
last = prev;
//判斷prev是否爲null
if (prev == null)
//如果prev爲null,表示鏈表中已無結點存在,則設置頭結點也爲null
first = null;
else
////如果prev不爲null,則設置prev結點的後結點爲null
prev.next = null;
//元素數量-1
size--;
//修改次數+1
modCount++;
//返回移除節點的內容element
return element;
}
(10) unlink()方法
//移除指定非空結點
E unlink(Node<E> x) {
//分別獲取要移除的結點的內容、前結點、後結點
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前結點爲null,表示移除的結點是頭結點
if (prev == null) {
//將後結點next設置爲新的頭結點
first = next;
//如果移除的結點不是頭結點
} else {
//那麼設置前結點的後結點爲後結點next
prev.next = next;
//並且設置結點x的前結點爲null
x.prev = null;
}
//如果後結點爲null,表示移除的結點是尾結點
if (next == null) {
//那麼將x的前結點prev設置爲尾結點
last = prev;
} else {
//如果後結點不爲null,就將後結點的前結點設置爲x的前結點prev
next.prev = prev;
//並且將x的後結點置爲null
x.next = null;
}
//此時的x結點,前結點爲null,後結點爲null,再將值item設置爲null,整個Node對象就爲null了,以便GC回收
x.item = null;
//結點數量-1
size--;
//修改次數+1
modCount++;
//返回移除結點x的item值element
return element;
}
(11) remove()方法
//移除一個內容爲O的結點
public boolean remove(Object o) {
//先判斷O是否爲null,如果是,則遍歷的時候,用==進行比較
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//調用移除結點的方法unlink()
unlink(x);
return true;
}
}
//判斷O不爲null,則遍歷的時候,用equals進行比較
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
實際上到這個地方,LinkedList的內部方法基本就介紹完畢, 其他的方法基本都是進行了一些判斷後,調用了上面的這些方法。我們再看幾個常用的普通方法源碼就一目瞭然。
(12) add()系列方法
//添加一個結點e
public boolean add(E e) {
//直接調用linkLast()在鏈表末尾添加結點
linkLast(e);
//如果成功則返回true
return true;
}
//在指定下標添加一個結點e
public void add(int index, E element) {
//判斷是否越界,條件是index >= 0並且index <= size,源碼如下
checkPositionIndex(index);
//如果index剛好等於size,表示是添加的尾結點,直接調用linkLast()方法
if (index == size)
linkLast(element);
else
//如果添加的結點不是尾結點,就直接調用linkBefore()方法
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
//從指定下標index開始,添加集合C中所有的元素到鏈表中
public boolean addAll(int index, Collection<? extends E> c) {
//老規矩,先判斷下標是否越界
checkPositionIndex(index);
//將集合c轉爲Object數組a
Object[] a = c.toArray();
//得到a的長度numNew
int numNew = a.length;
//如果長度爲0,那麼表示集合c爲null,直接返回false
if (numNew == 0)
return false;
//聲明兩個Node變量pred和succ
Node<E> pred, succ;
//下面這個if判斷,主要是爲了獲得插入座標插入新Node後的前後結點
//如果index剛好等於size,也就是說是從鏈表尾部開始添加,那麼插入段的前結點就是原鏈表的尾結點last,插入段的尾結點就爲null
if (index == size) {
succ = null;
pred = last;
//如果是從鏈表中間插入,則插入段的尾結點就是插入前原鏈表index位置上的結點,而插入段的前結點就是原鏈表Index位置上的結點的前結點
} else {
succ = node(index);
pred = succ.prev;
}
//開始循環插入結點
for (Object o : a) {
//創建新結點newNode,內容爲e,前結點就是我們上面記錄的前結點pred,後結點是下一個新結點(如果還有的話)
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
//判斷前結點pred是否爲null,如果是,那麼這個新結點newNode就是頭結點first
if (pred == null)
first = newNode;
//如果pred不爲nul,那麼就直接設置前結點的後結點爲當前新結點newNode
else
pred.next = newNode;
//將新的結點又設置爲下一個節點的前結點
pred = newNode;
}
//循環結束後,判斷尾結點是否爲null,如果尾結點succ爲null,表示最後添加的結點(上一行代碼可知被設置爲了pred)就是整個新鏈表的尾結點last
if (succ == null) {
last = pred;
//如果succ不爲null,表示是在原鏈表中間插入的,那麼設置記錄的前結點的後結點爲尾結點;設置記錄的尾結點設置給後結點的前結點。上面過程中,succ在被記錄後是一直沒變的,pred會一直被指向最新的結點。
} else {
pred.next = succ;
succ.prev = pred;
}
//結點數據增加數組a的長度
size += numNew;
//修改記錄+1
modCount++;
//返回true表示添加成功
return true;
}
以上方法中addAll(int index, Collection<? extends E> c)方法可能看代碼有些晦澀,但是腦海中有整個過程模型,就很好理解了。如下圖:
(13) get()方法
public E get(int index) {
//檢查是否越界
checkElementIndex(index);
//返回對應下標的Node結點的item
return node(index).item;
}
Node<E> node(int index) {
//判斷座標index是否小於1/2的size,也就是說在前半部分的話,就只用正向遍歷前半段鏈表
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類的實例在查找結點元素的時候,巧妙的利用了自己雙向鏈表的特點,雖然時間複雜度還是O(n),但是也算是巨大的提升了。
(14) clone()方法
public Object clone() {
LinkedList<E> clone = superClone();
//初始化克隆後的對象clone
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
//循環往clone中添加元素的引用,所以這是淺克隆
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
(15) toArray()系列方法
//直接將當前的鏈表轉化爲數組
public Object[] toArray() {
//創建鏈表長度size的Object數組result
Object[] result = new Object[size];
int i = 0;
//循環往result中添加元素
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
//將當前鏈表的內容轉化到指定數組a中
public <T> T[] toArray(T[] a) {
//先判斷傳入的數組a能否裝得下整個鏈表,裝不下則重新創建一個數組
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
//從座標0開始往數組a中放入鏈表元素,是直接替換,而不是增加
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
從代碼中可以看出,數組a的length小於等於size時,a中所有元素被覆蓋,被拓展來的空間存儲的內容都是null;若數組a的length的length大於size,則0至size-1位置的內容被覆蓋,size位置的元素被設置爲null,size之後的元素不變。
除了以上的方法外,LinkedList類除了Node以外,還有兩個內部類分別是ListItr類和DescendingIterator類。我們再來詳細看下源碼:
(16) ListItr內部類
// 最近一次返回的節點,也是當前持有的節點
private Entry<E> lastReturned = header;
// 對下一個元素的引用
private Entry<E> next;
// 下一個節點的index
private int nextIndex;
private int expectedModCount = modCount;
// 構造方法,接收一個index參數,返回一個ListItr對象
ListItr(int index) {
// 如果index小於0或大於size,拋出IndexOutOfBoundsException異常
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
// 判斷遍歷方向
if (index < (size >> 1)) {
// next賦值爲第一個節點
next = header.next;
// 獲取指定位置的節點
for (nextIndex=0; nextIndex<index; nextIndex++)
next = next.next;
} else {
// else中的處理和if塊中的處理一致,只是遍歷方向不同
next = header;
for (nextIndex=size; nextIndex>index; nextIndex--)
next = next.previous;
}
}
// 根據nextIndex是否等於size判斷時候還有下一個節點(也可以理解爲是否遍歷完了LinkedList)
public boolean hasNext() {
return nextIndex != size;
}
// 獲取下一個元素
public E next() {
checkForComodification();
// 如果nextIndex==size,則已經遍歷完鏈表,即沒有下一個節點了(實際上是有的,因爲是循環鏈表,任何一個節點都會有上一個和下一個節點,這裏的沒有下一個節點只是說所有節點都已經遍歷完了)
if (nextIndex == size)
throw new NoSuchElementException();
// 設置最近一次返回的節點爲next節點
lastReturned = next;
// 將next“向後移動一位”
next = next.next;
// index計數加1
nextIndex++;
// 返回lastReturned的元素
return lastReturned.element;
}
public boolean hasPrevious() {
return nextIndex != 0;
}
// 返回上一個節點,和next()方法相似
public E previous() {
if (nextIndex == 0)
throw new NoSuchElementException();
lastReturned = next = next.previous;
nextIndex--;
checkForComodification();
return lastReturned.element;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex-1;
}
// 移除當前Iterator持有的節點
public void remove() {
checkForComodification();
Entry<E> lastNext = lastReturned.next;
try {
LinkedList.this.remove(lastReturned);
} catch (NoSuchElementException e) {
throw new IllegalStateException();
}
if (next==lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = header;
expectedModCount++;
}
// 修改當前節點的內容
public void set(E e) {
if (lastReturned == header)
throw new IllegalStateException();
checkForComodification();
lastReturned.element = e;
}
// 在當前持有節點後面插入新節點
public void add(E e) {
checkForComodification();
// 將最近一次返回節點修改爲header
lastReturned = header;
addBefore(e, next);
nextIndex++;
expectedModCount++;
}
// 判斷expectedModCount和modCount是否一致,以確保通過ListItr的修改操作正確的反映在LinkedList中
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
ListItr實現了ListIterator接口,可知它是一個迭代器,通過它可以遍歷修改LinkedList。 在LinkedList中提供了獲取ListItr對象的方法:listIterator(int index)。
(17) DescendingIterator內部類
// 獲取ListItr對象
final ListItr itr = new ListItr(size());
// hasNext其實是調用了itr的hasPrevious方法
public boolean hasNext() {
return itr.hasPrevious();
}
// next()其實是調用了itr的previous方法
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
這是一個反向的Iterator,也是調用的ListItr類中的方法。
三、總結
LinkedList類是基於雙向鏈表實現的,並且在內存中不連續,使用上沒有大小限制,理論上可以無限增加,所以LinkedList類中也沒有擴容方法;此外LinkedList類不具備隨機訪問,插入和刪除效率較高,但是查找效率較低。LinkedList類允許結點的元素可以爲null,也允許重複,並且也有modCount來實現快速失敗的機制。雙向鏈表由於可以反向遍歷,相較於單向鏈表在某些操作上具有性能優勢,但是由於每個結點都需要額外的內存空間來存儲前驅指針,所以雙向鏈表相對來說需要佔用更多的內存空間,這也是空間換時間的一種體現。
更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!