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的實現。