java中的數據結構源碼解析的系列文章:
ArrayList源碼分析
LinkedList與Queue源碼分析java
上篇已經分析了基於數組實現數據存儲的ArrayList(線性表),而本篇的主角是LinkedList,這個使用了鏈表實現數據存儲的集合,它的增、刪、查、改方式又會是怎樣的呢?下面就開始對LinkedList的源碼進行分析吧。node
在分析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);
...
}複製代碼
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;
...
}複製代碼
LinkedList的成員變量不多,就上面那3個,其中first和last都是Node類型(即節點類型),用來表示鏈表的頭和尾,這跟ArrayList就存在着本質的區別了。bash
要注意:
first和last僅僅只是節點而已,跟數據元素沒有關係,能夠認爲就是2個額外的"指針",分別指着鏈表的頭和尾。
數據結構
public LinkedList() {
}複製代碼
LinkedList的構造函數有2個,以平時最經常使用的構造函數爲例,發現該構造函數什麼事都沒作。函數
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的使用。好像扯遠了~post
好了,那下面就看看LinkedList是怎麼進行增、刪、改、查的。優化
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()方法,它對元素進行了真正添加操做,分爲如下幾個步驟:ui
經過對add(E e)方法的分析,咱們也知道了,原來LinkedList中的元素就是一個個的節點(Node),而真正的數據則存放在Node之中(數據被Node的item所引用)。
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集合中間或頭部添加元素分爲如下幾個步驟:
對於鏈表的操做仍是有些複雜的,特別是這種雙向鏈表,不過仔細理解下,也不是什麼問題(看不懂的能夠邊看步驟邊動手畫一畫)。到這裏,對於LinkedList的第一個添加方法就分析完了。
這也是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與集合長度的一半作比較,來肯定是在集合的前半段仍是後半段進行查找。
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),刪除操做分如下幾個步驟:
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)的分析在第一個刪除方法中已經提到了,可往回再看看。
LinkedList集合對數據的獲取與修改均經過node(int index)方法來執行日後的操做,關於node(int index)方法的分析也已經在第一個添加方法的時候已經提過,這裏也就再也不囉嗦了。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}複製代碼
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}複製代碼
這裏要順帶分析下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的接口聲明:
咱們知道,隊列是先進先出的,添加元素只能從隊尾添加,刪除元素只能從隊頭刪除,Queue中的方法就體現了這種特性。
public interface Queue<E> extends Collection<E> {
boolean offer(E e);
E poll();
E peek();
...
}複製代碼
從上面這幾個方法出發,來看看LinkedList是如何實現的。
public boolean offer(E e) {
return add(e);
}複製代碼
能夠看到,在LinkedList中,隊列的offer(E e)方法其實是調用了LinkedList的add(E e),add(E e)已經在最前面分析過了,就是在鏈表的尾部添加一個元素~
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)差很少且相對簡單,這裏不作過多說明。
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}複製代碼
peek()先經過first拿到隊頭元素,而後取出元素中的數據實體返回而已。