上篇文章我們分析了常見的ArrayList源碼茅郎,它的內(nèi)部是由一個(gè)數(shù)組來實(shí)現(xiàn)的患雏。那么今天,我們來分析另一個(gè)常見的類LinkedList捏萍。本文分析都來自Java8太抓。
(ps:這段話寫自寫完本文記錄后添加。個(gè)人感想為已經(jīng)寫成了介紹鏈表)
類說明
不多廢話令杈,首先我們來看一下這個(gè)類走敌。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
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這個(gè)類名我們就猜出,這個(gè)List內(nèi)部可能是由鏈表來實(shí)現(xiàn)逗噩,我們后面來驗(yàn)證一下掉丽。它實(shí)現(xiàn)了Deque接口跌榔,因此可以知道,LinkedList也可以作為隊(duì)列來進(jìn)行使用机打。同時(shí)也實(shí)現(xiàn)了Serializable接口矫户,說明可以對(duì)它進(jìn)行序列化,反序列化來傳遞残邀,獲取數(shù)據(jù)等等皆辽。
我們?cè)賮砜纯碙inkedList的成員變量。首先是一個(gè)int類型的size,這個(gè)我們?cè)贏rrayList中也介紹過芥挣。這個(gè)是一個(gè)表明List有多少個(gè)元素驱闷。然后是一個(gè)Node類型的first變量,從注釋我們看出指向頭結(jié)點(diǎn)的變量(用C空免,C++解釋稱之為指針空另,也許用C語(yǔ)言的方式更好解釋)。后面的last變量也很好理解蹋砚,它指向最后結(jié)點(diǎn)扼菠。我們跟進(jìn)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;
}
}
這是一個(gè)LinkedList的內(nèi)部類,還是比較簡(jiǎn)單的坝咐。首先一個(gè)泛型的item變量循榆,用來存儲(chǔ)數(shù)據(jù),然后有一個(gè)指向下一個(gè)結(jié)點(diǎn)的next指針墨坚,前一個(gè)結(jié)點(diǎn)的prev指針秧饮。
至此我們其實(shí)就可以得出結(jié)論,LinkedList果然名不虛傳泽篮,內(nèi)部就是由鏈表的形式實(shí)現(xiàn)盗尸。它并沒有用數(shù)組來存儲(chǔ)數(shù)據(jù)元素,而是由一個(gè)個(gè)Node類型結(jié)點(diǎn)來儲(chǔ)存數(shù)據(jù)帽撑,然后每個(gè)Node結(jié)點(diǎn)通過指向前后結(jié)點(diǎn)的next和prev指針將整個(gè)List串聯(lián)起來泼各。我們來畫張簡(jiǎn)單圖來看看ArrayList和LinkedList的基本區(qū)別:
正是這樣的區(qū)別,從而導(dǎo)致了兩者不同的效率問題亏拉。如果我們對(duì)一個(gè)ArrayList頻繁的增加數(shù)據(jù)历恐,那么內(nèi)部的數(shù)組就會(huì)不斷擴(kuò)容創(chuàng)建新的數(shù)組然后把舊數(shù)據(jù)復(fù)制到新數(shù)組返回。插入數(shù)據(jù)或者刪除其中一個(gè)數(shù)據(jù)专筷,又要把所有數(shù)據(jù)后移或者前移。這樣的操作效率很低下的蒸苇,為了一個(gè)數(shù)據(jù)處理了其他數(shù)據(jù)磷蛹。
而LinkedList不同,插入只需要將要插入位置的前結(jié)點(diǎn)的next指針指向新的數(shù)據(jù)結(jié)點(diǎn)(如下圖插入工作圖的2)溪烤,將插入結(jié)點(diǎn)的prev指向前指針(如1處)味咳,將next指向下一個(gè)結(jié)點(diǎn)(如3處)庇勃,將之前的后結(jié)點(diǎn)的prev指針指向插入指針(如4處)。刪除也是如下刪除工作圖槽驶,就不再講述责嚷。這樣的話只需要幾步操作就完成了處理,無需移動(dòng)大量數(shù)據(jù)掂铐。效率非常高罕拂。(下面兩幅圖均來自網(wǎng)上大神的鏈表介紹文章http://www.cnblogs.com/bb3q/p/4490589.html)無恥的借用一下下~~~
那既然如此ArrayList豈不是一無是處了?為啥還要用呢全陨?其實(shí)并不是爆班,鏈表既然是用指針的方式連起來,那么意味著我們尋找某個(gè)結(jié)點(diǎn)就需要從頭開始遍歷辱姨,直到找到這個(gè)元素為止柿菩,并不能提供隨機(jī)訪問。例如我們想找數(shù)組的第五個(gè)元素雨涛,那么只需xxx[4]即可得到枢舶,但是鏈表不行,只能從頭開始遍歷到第五個(gè)元素才行替久。因此在開發(fā)中如果需要對(duì)list頻繁的添加凉泄,刪除,插入侣肄,那么用LinkedList是很好的旧困。但是數(shù)據(jù)量不大,需要經(jīng)常查詢數(shù)據(jù)的時(shí)候稼锅,ArrayList更適合吼具。
至此我們就將LinkedList的類和成員變量介紹完了,也分析了和ArrayList的區(qū)別矩距。接下來我們來看看構(gòu)造方法:
public 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);
}
第一個(gè)構(gòu)造方法是空實(shí)現(xiàn)拗盒,因?yàn)閮?nèi)部是鏈表,無需像ArrayLsi一樣t實(shí)例化數(shù)組锥债。
我們來看看第二個(gè)構(gòu)造方法陡蝇,傳入?yún)?shù)為一個(gè)集合,主要是調(diào)用了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;
}
第一個(gè)addAll()方法將size傳入登夫,其實(shí)就是從原有List的末尾開始添加傳入的數(shù)據(jù),而這里size為0允趟。所以就是從頭開始添加數(shù)據(jù)恼策。我們仔細(xì)分析第二個(gè)addAll()方法。
我們拆開來分析:
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
首先checkPositionIndex()方法來判斷傳入的index是否大于0并且小于size潮剪。
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;
}
然后將傳入的集合首先轉(zhuǎn)換為數(shù)組涣楷。然后判斷數(shù)組長(zhǎng)度是否為0分唾,如果是0,表示沒有添加的數(shù)據(jù)狮斗,直接返回绽乔。
如果不是,那么定義兩個(gè)結(jié)點(diǎn)succ和pred碳褒。個(gè)人理解succ表示要插入的index下標(biāo)的結(jié)點(diǎn)折砸,pred表示succ結(jié)點(diǎn)的前結(jié)點(diǎn)。然后判斷index是否等于size,而我們?cè)跇?gòu)造方法中傳入的index就等于size,所以表示從鏈表的末尾開始添加數(shù)據(jù)骤视。即succ為null(因?yàn)橄聵?biāo)從0開始鞍爱,所以最后一個(gè)數(shù)據(jù)下標(biāo)為size - 1),pred為last最后一個(gè)結(jié)點(diǎn)。如果index不等于size,那么找到index處的結(jié)點(diǎn)賦值給succ,pred等于succ的前結(jié)點(diǎn)专酗。
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;
}
到這里就已經(jīng)把預(yù)處理工作完成了睹逃。接下來就是添加數(shù)據(jù)過程。首先是一個(gè)for循環(huán)遍歷a數(shù)組祷肯。每個(gè)對(duì)象(也就是數(shù)據(jù))都生成一個(gè)結(jié)點(diǎn)沉填。然后將新結(jié)點(diǎn)的前指針指向pred結(jié)點(diǎn)。如果pred為空(這種情況就是鏈表為空佑笋,沒有一個(gè)結(jié)點(diǎn))翼闹,那么這個(gè)新結(jié)點(diǎn)就是鏈表的表頭。如果pred不為空蒋纬,那么將pred的后指針指向這個(gè)新結(jié)點(diǎn)猎荠。這樣就將pred結(jié)點(diǎn)和新結(jié)點(diǎn)串聯(lián)起來。然后pred往后移一個(gè)結(jié)點(diǎn)蜀备,指向新結(jié)點(diǎn)关摇。這樣就不斷的將所有新數(shù)據(jù)插入到指點(diǎn)位置往后。
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
for循環(huán)后碾阁,會(huì)判斷succ是否為空(即我們插入位置的舊結(jié)點(diǎn)是否為空)输虱。如果為空,表明我們是插入到鏈表的末尾的脂凶,那么就無需將舊結(jié)點(diǎn)的后結(jié)點(diǎn)的prev指針修改(因?yàn)楦緵]后結(jié)點(diǎn))宪睹,直接將last指向pred(因?yàn)樵趂or循環(huán)中pred會(huì)指向新數(shù)據(jù)集合的最后一個(gè)數(shù)據(jù))即可。如果succ不為空蚕钦,表示我們是在鏈表的中間插入數(shù)據(jù)的亭病。因此就要將新數(shù)據(jù)集合的最后一個(gè)數(shù)據(jù)結(jié)點(diǎn)的next后指針指向我們插入位置的結(jié)點(diǎn)。然后將未插入前的index處的結(jié)點(diǎn)的前結(jié)點(diǎn)修改為指向新插入的最后一個(gè)數(shù)據(jù)結(jié)點(diǎn)即pred嘶居。
size += numNew;
modCount++;
return true;
最后將數(shù)據(jù)數(shù)量加上新插入的數(shù)據(jù)數(shù)量命贴,修改數(shù)據(jù)結(jié)構(gòu)次數(shù)自加1即可。
至此我們就已經(jīng)將構(gòu)造方法分析完畢,可以說就是最常見的數(shù)據(jù)結(jié)構(gòu)中雙向鏈表的操作方法胸蛛。接下來我們繼續(xù)分析一些常見的方法。
add()
我們來看看最常用的幾個(gè)add()方法,首先是第一個(gè):
public boolean add(E e) {
linkLast(e);
return true;
}
我們看到就是調(diào)用了linkLast()方法樱报,我們跟進(jìn)去看看:
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++;
}
我們來看看這個(gè)方法葬项,首先定義了兩個(gè)常量l和newNode結(jié)點(diǎn)。分別表示鏈表中最后一個(gè)結(jié)點(diǎn)和一個(gè)封裝了新數(shù)據(jù)的新結(jié)點(diǎn)迹蛤。新結(jié)點(diǎn)的前指針指向l民珍。然后將last指針指向新結(jié)點(diǎn)。然后判斷如果l結(jié)點(diǎn)(即之前的最后一個(gè)結(jié)點(diǎn))是空盗飒,表明鏈表之前沒有數(shù)據(jù)嚷量,加入的新數(shù)據(jù)將會(huì)稱為第一個(gè)結(jié)點(diǎn)。因此將表頭first指針指向新結(jié)點(diǎn)逆趣。如果l不為空蝶溶。則將之前最后一個(gè)結(jié)點(diǎn)的后指針指向新結(jié)點(diǎn),新結(jié)點(diǎn)指向l宣渗。然后size自加1抖所,modCount自加1表示修改了一次鏈表。這樣就將新數(shù)據(jù)插入到了鏈表的末尾痕囱。
第二個(gè)add()方法:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
首先是調(diào)用checkPositionIndex()方法檢查index是否大于0并小于size田轧。
然后如果index等于size,表示我們要將數(shù)據(jù)插入到鏈表的末尾鞍恢,調(diào)用linkLast()方法來插入數(shù)據(jù),上面分析了該方法這里就不在贅述傻粘。如果index不等于size,那么表示是在鏈表的中間插入數(shù)據(jù),那么調(diào)用linkBefore()方法:
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++;
}
可以看到其實(shí)也是差不多的,首先定義兩個(gè)常量Node變量pred和newNode用來表示我們要插入的結(jié)點(diǎn)的前結(jié)點(diǎn)和一個(gè)要插入的新結(jié)點(diǎn)帮掉,這個(gè)新結(jié)點(diǎn)的前指針指向pred,后指針指向插入位置的舊結(jié)點(diǎn)弦悉。然后將要插入位置的舊結(jié)點(diǎn)的prev前指針指向新結(jié)點(diǎn)。如果pred為空旭寿,表示我們要插入的位置是表頭警绩,直接將表頭指針first指向新結(jié)點(diǎn)。如果不為空盅称,那么將pred的后指針next指向新結(jié)點(diǎn)肩祥。
remove()
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;
}
我們來看看第一個(gè)remove()方法,傳入的參數(shù)表示是一個(gè)數(shù)據(jù)缩膝。雖然要首先判斷我們傳入的數(shù)據(jù)是否為空來分開操作混狠。但是其實(shí)都是一樣的做遍歷然后刪除結(jié)點(diǎn)。如果是空疾层,那么就要進(jìn)行for循環(huán)從表頭開始遍歷将饺,有人可能會(huì)好奇,傳入為空的話為什么還要找,沒有意義呀予弧?這里我的想法是鏈表是一種數(shù)據(jù)結(jié)構(gòu)刮吧,它是以結(jié)點(diǎn)為基礎(chǔ)存在的,不關(guān)心數(shù)據(jù)掖蛤。所以有可能某個(gè)結(jié)點(diǎn)不為空杀捻,但是結(jié)點(diǎn)封裝的數(shù)據(jù)為空。因此蚓庭,在某些特殊場(chǎng)景下萬一有些人就是要存空數(shù)據(jù)呢致讥?我們重點(diǎn)看看unlink()方法:
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;
}
首先將要?jiǎng)h除結(jié)點(diǎn)的數(shù)據(jù)取出。然后得到刪除結(jié)點(diǎn)的前結(jié)點(diǎn)prev器赞,后結(jié)點(diǎn)next垢袱。然后如果前結(jié)點(diǎn)為空,表明刪除結(jié)點(diǎn)為頭結(jié)點(diǎn)港柜,因此直接將表頭first指針指向刪除結(jié)點(diǎn)的后結(jié)點(diǎn)请契。否則的話將prev的next指針直接跳過刪除結(jié)點(diǎn)指向next結(jié)點(diǎn)。然后將輸出結(jié)點(diǎn)的prev指針置空潘懊。后面的next判空也是同理姚糊。最后將刪除結(jié)點(diǎn)的數(shù)據(jù)置空,鏈表數(shù)量自減1授舟,modCount自加1救恨,然后刪除結(jié)點(diǎn)的數(shù)據(jù)。至此释树,結(jié)點(diǎn)刪除就已經(jīng)完成肠槽。
接下來我們來看看第二個(gè)remove()方法:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
這個(gè)更簡(jiǎn)單,移除指定的結(jié)點(diǎn)奢啥。還是首先檢查index的合法性秸仙,然后得到指定位置的結(jié)點(diǎn)調(diào)用unlink()方法移除即可。上面已分析unlink()方法桩盲,就不再贅述寂纪。
第三個(gè)remove()方法:
public E remove() {
return removeFirst();
}
說實(shí)話這個(gè)remove()方法我不是很理解為什么這些寫,就是調(diào)用removeFirst()方法來移除表頭結(jié)點(diǎn)赌结。百思不得其解為什么要這么設(shè)計(jì)捞蛋,而且remove()這個(gè)方法名也根本看不出是移除表頭的意思。也沒有什么可以分析的柬姚,就是一個(gè)簡(jiǎn)單的移除表頭結(jié)點(diǎn)拟杉。
clear()
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
我們來看看clear()方法,顧名思義量承,clear就是將鏈表清空搬设。所以直接一個(gè)for循環(huán)穴店,從表頭開始一直到表尾。將每個(gè)結(jié)點(diǎn)的數(shù)據(jù)直接置空即可拿穴。最后將表頭泣洞,表尾first,last置空。size置為0即可贞言。
get()
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
get()方法也很常用斜棚,我們來看看,首先調(diào)用checkElementIndex()方法檢查index是否大于0并小于size该窗。然后調(diào)用了node()方法來獲取指定位置的結(jié)點(diǎn)。我們?nèi)タ纯矗?/p>
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;
}
}
我們看到蚤霞,這里還是用了一點(diǎn)小技巧酗失。首先判斷我們想要的位置是否是在鏈表的前半部分還是后半部分(size >> 1 表示除以2),如果在前半部分那么就從表頭開始遍歷昧绣,在后半部分就從表尾開始往前遍歷规肴。知道拿到目標(biāo)結(jié)點(diǎn)即可。
set()
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
首先也是調(diào)用checkElementIndex()方法檢查index是否大于0并小于size夜畴。然后也是調(diào)用node()方法獲取指定位置的結(jié)點(diǎn)拖刃。然后將指定結(jié)點(diǎn)的item數(shù)據(jù)置為新值,返回舊值即可贪绘。
至此我們常見的LinkedList的方法源碼分析就已經(jīng)完了兑牡,其他的一些方法要么不怎么用,要么非常簡(jiǎn)單只有一兩行簡(jiǎn)單代碼甚至就是調(diào)用這些常見方法税灌,讀者一跟進(jìn)去就能明白均函,這里就不再深究。
最后我們?cè)賮砜偨Y(jié)一下:
首先LinkedList內(nèi)部是由雙向鏈表來實(shí)現(xiàn)的菱涤。我們儲(chǔ)存的每一個(gè)數(shù)據(jù)都會(huì)被封裝在一個(gè)數(shù)據(jù)結(jié)點(diǎn)之中苞也。而結(jié)點(diǎn)瘋轉(zhuǎn)了指向前結(jié)點(diǎn)的指針,數(shù)據(jù)粘秆,指向后結(jié)點(diǎn)的指針如迟。依靠這些數(shù)據(jù)結(jié)點(diǎn)實(shí)現(xiàn)雙向鏈表。
既然是鏈表攻走,那么優(yōu)點(diǎn)就是添加殷勘,插入,刪除數(shù)據(jù)效率比數(shù)組高很多陋气。因?yàn)樵诓迦牖蛘邉h除某個(gè)數(shù)據(jù)時(shí)劳吠,只需對(duì)要?jiǎng)h除結(jié)點(diǎn),前結(jié)點(diǎn)巩趁,后結(jié)點(diǎn)進(jìn)行操作痒玩,無需像數(shù)組一樣將后續(xù)數(shù)據(jù)全部前移或者后移淳附。但是由此也看出缺點(diǎn),因?yàn)殒湵聿⒉皇沁B續(xù)的空間儲(chǔ)存蠢古,也沒有什么下標(biāo)進(jìn)行記錄位置奴曙。因此要尋找某個(gè)數(shù)據(jù)時(shí)只能進(jìn)行遍歷,而不像數(shù)組一樣可以隨機(jī)查找草讶。如果我們?cè)趯?shí)際開發(fā)中我們需要對(duì)某個(gè)List進(jìn)行頻繁的插入洽糟,刪除,而且數(shù)據(jù)量又特別大的時(shí)候堕战±だ#可以考慮使用LinkedList。