java中的資料結構原始碼解析的系列文章:
ArrayList原始碼分析
LinkedList與Queue原始碼分析
一、簡述
上篇已經分析了基於陣列實現資料儲存的ArrayList(線性表),而本篇的主角是LinkedList,這個使用了連結串列實現資料儲存的集合,它的增、刪、查、改方式又會是怎樣的呢?下面就開始對LinkedList的原始碼進行分析吧。
二、分析
List
在分析LinkedList之前,還是先瞄一眼List介面,雖然前篇已經看過一遍了,但為了明確下文的分析方向,還是先把List介面中的幾個增刪改查方法再列一次。
public interface List<E> extends Collection<E> {
boolean add(E e);
void add(int index, E element);
boolean remove(Object o);
E remove(int index);
E set(int index, E element);
E get(int index);
...
}複製程式碼
LinkedList
1、成員變數
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
...
}複製程式碼
- size:陣列元素個數
- first:頭節點
- last:尾節點
LinkedList的成員變數很少,就上面那3個,其中first和last都是Node型別(即節點型別),用來表示連結串列的頭和尾,這跟ArrayList就存在著本質的區別了。
要注意:
first和last僅僅只是節點而已,跟資料元素沒有關係,可以認為就是2個額外的"指標",分別指著連結串列的頭和尾。
2、建構函式
1)LinkedList
public LinkedList() {
}複製程式碼
LinkedList的建構函式有2個,以平時最常用的建構函式為例,發現該建構函式什麼事都沒做。
2)Node
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類是在LinkedList類中的,也就是一個內部類,若不使用static修飾,那麼Node就是一個普通的內部類,在java中,一個普通內部類在例項化之後,預設會持有外部類的引用,這就有可能造成記憶體洩露。但使用static修飾過的內部類(稱為靜態內部類),就不會有這種問題,在Android中,有很多這樣的情況,如Handler的使用。好像扯遠了~
好了,那下面就看看LinkedList是怎麼進行增、刪、改、查的。
3、增
1)add(E e)
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}複製程式碼
因為LinkedList是連結串列結構,所以每新增一個元素就是讓這個元素連結到連結串列的尾部。
add(E e)的核心是linkLast()方法,它對元素進行了真正新增操作,分為以下幾個步驟:
- 先讓此時集合中的尾節點(即last"指標"指向的節點)賦給變數 l 。
- 然後,建立一個新節點,結合Node的建構函式,我們可以知道,在建立新節點(newNode)的同時,newNode的prev指向了l(即之前集合中的尾節點),變數 l 就是newNode的前驅節點了,newNode的後繼節點為null。
- 再將last指向newNode,也就是說newNode成為該連結串列新的末尾節點。
- 接著,判斷變數 l 是否為null,若是null,說明之前集合中沒有元素(此時newNode是集合中唯一一個元素),則將first指向newNode,也就是說此時的newNode既是頭節點又是尾節點(要知道,這時newNode中的prev和next均是null,但被first和last同時指向);
若變數 l 不是null,說明之前集合中已經存在了至少一個元素,則讓之前集合中的尾節點(即變數 l )的next指向newNode。(結合步驟2,此時的newNode與newNode的前驅節點 l 已經是相互指向了) - 最後,跟ArrayList一樣,讓記錄集合長度的size加1。
通過對add(E e)方法的分析,我們也知道了,原來LinkedList中的元素就是一個個的節點(Node),而真正的資料則存放在Node之中(資料被Node的item所引用)。
2)add(int index, E element)
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}複製程式碼
該add方法將新增集合元素分為2種情況,一種是在集合尾部新增,另一種是在集合中間或頭部新增,因為第一種情況也是呼叫linkLast()方法,這裡不再囉嗦,我們看看第二種情況,分析linkBefore(E e, 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++;
}複製程式碼
往LinkedList集合中間或頭部新增元素分為以下幾個步驟:
- 先呼叫node(int index)方法得到指定位置的元素節點,也就是linkBefore()方法中的形參 succ。
- 然後,通過succ.prevt得到succ的前一個元素pred。(此時拿到了第index個元素succ,和第index-1個元素pred)
- 再建立一個新節點newNode,newNode的prev指向了pred,newNode的next指向了succ。(即newNode往succ和pred的中間插入,並單向與它們分別建立聯絡,eg:pred ← newNode → succ)
- 再讓succ的prev指向newNode。(succ與跟newNode建立聯絡了,此時succ與newNode是雙向關聯,eg:pred ← newNode ⇋ succ)。
- 接著,判斷pred是否為null,若是null,說明之前succ是集合中的第一個元素(即index值為0),現在newNode跑到了succ前面,所以只需要將first指向newNode(eg:first ⇌ newNode ⇋ succ);
若pred不為null,則將pred的next指向newNode。(這時pred也主動與newNode建立聯絡了,此時pred與newNode也是雙向關聯,eg:pred ⇌ newNode ⇋ succ) - 最後,讓記錄集合長度的size加1。
對於連結串列的操作還是有些複雜的,特別是這種雙向連結串列,不過仔細理解下,也不是什麼問題(看不懂的可以邊看步驟邊動手畫一畫)。到這裡,對於LinkedList的第一個新增方法就分析完了。
下面是對node(int index)方法的分析:
這也是LinkedList獲取元素的核心方法,相當重要,因為後面會出現很多次,這裡就順帶先分析一下了。
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;
}
}複製程式碼
細看node(int index)方法中的程式碼邏輯,可以看到,它是通過遍歷的方式,將集合中的元素一個個拿出來,再通過該元素的prev或next拿到下一個遍歷的元素,經過index次迴圈後,最終才拿到了index對應的元素。
跟ArrayList相比,因為ArrayList底層是陣列實現,擁有下標這個特性,在獲取元素時,不需要對集合進行遍歷,所以查詢某個元素會特別快(在資料量特別多的情況下,ArrayList和LinkedList在效率上的差別就相當明顯了)。
不過,LinkedList對元素的獲取還是做了一定優化的,它對index與集合長度的一半做比較,來確定是在集合的前半段還是後半段進行查詢。
4、刪
1)remove(int index)
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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(int index)這個方法中,先通過index和node(int index)拿到了要被刪除的元素x,然後呼叫了unlink(Node x)方法將其刪除,自然,LinkedList刪除元素的核心方法就是unlink(Node x),刪除操作分以下幾個步驟:
- 通過要刪除的元素x拿到它的前驅節點prev和後繼節點next。
- 若前驅節點prev為null,說明x是集合中的首個元素,直接將first指向後繼節點next即可;
若不為null,則讓前驅節點prev的next指向後繼節點next,再將x的prev置空。(這時prev與x的關聯就解除了,並與next建立了聯絡)。 - 若後繼節點next為null,說明x是集合中的最後一個元素,直接將last指向前驅節點prev即可;(下圖分別對應步驟2中的兩種情況)
若不為null,則讓後繼節點next的prev指向前驅節點prev,再將x的next置空。(這時next與x的關聯就解除了,並與prev建立了聯絡)。 - 最後,讓記錄集合長度的size減1。
2)remove(Object o)
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;
}複製程式碼
remove(Object o)這個刪除元素的方法的形參o是資料本身,而不是LinkedList集合中的元素(節點),所以需要先通過節點遍歷的方式,找到o資料對應的元素,然後再呼叫unlink(Node x)方法將其刪除,關於unlink(Node x)的分析在第一個刪除方法中已經提到了,可往回再看看。
5、查 & 改
LinkedList集合對資料的獲取與修改均通過node(int index)方法來執行往後的操作,關於node(int index)方法的分析也已經在第一個新增方法的時候已經提過,這裡也就不再囉嗦了。
1)set(int index, E element)
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}複製程式碼
2)get(int index)
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}複製程式碼
三、佇列Queue
這裡要順帶分析下java中的佇列實現,why?因為java中佇列的實現就是LinkedList,你可能會疑問,佇列的英文是Queue,在java中也有對應的介面,怎麼會跟LinkedList扯上關係呢?因為LinkedList實現了佇列:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
...
}複製程式碼
程式碼中的Deque是Queue的一個子介面,它繼承了Queue:
public interface Deque<E> extends Queue<E> {...}複製程式碼
從這兩者的關係,不難得出,佇列的實現方式也是連結串列。下面先來看看Queue的介面宣告:
1、Queue
我們知道,佇列是先進先出的,新增元素只能從隊尾新增,刪除元素只能從隊頭刪除,Queue中的方法就體現了這種特性。
public interface Queue<E> extends Collection<E> {
boolean offer(E e);
E poll();
E peek();
...
}複製程式碼
- offer():新增隊尾元素
- poll():刪除隊頭元素
- peek():獲取隊頭元素
從上面這幾個方法出發,來看看LinkedList是如何實現的。
2、LinkedList對Queue的實現
1)增
public boolean offer(E e) {
return add(e);
}複製程式碼
可以看到,在LinkedList中,佇列的offer(E e)方法實際上是呼叫了LinkedList的add(E e),add(E e)已經在最前面分析過了,就是在連結串列的尾部新增一個元素~
2)刪
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(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;
}複製程式碼
poll()方法先拿到隊頭元素 f ,若 f 不為null,就呼叫unlinkFirst(Node f)其刪除。unlinkFirst(Node f)在實現上跟unlink(Node x)差不多且相對簡單,這裡不做過多說明。
3)查
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}複製程式碼
peek()先通過first拿到隊頭元素,然後取出元素中的資料實體返回而已。
四、總結
- LinkedList是基於連結串列實現的,並且是雙向連結串列。
- LinkedList中的元素就是一個個的節點,而真正的資料則存放在Node之中。
- LinkedList通過遍歷的方式獲取集合中的元素,效率比ArrayList低。
- Queue佇列的實現方式也是連結串列,java中,LinkedList是Queue的實現。