05-LinkedList 源碼解析(集合)

注:源碼系列文章主要是對(duì)某付費(fèi)專欄的總結(jié)記錄剔交。如有侵權(quán)词爬,請(qǐng)聯(lián)系刪除料祠。

LinkedList 適用于集合元素先入先出和先入后出的場(chǎng)景骆捧,在隊(duì)列源碼中被頻繁使用,面試也經(jīng)常被問到髓绽。

1 整體架構(gòu)

LinkedList 底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)雙向鏈表敛苇,整體結(jié)構(gòu)如下圖所示:

LinkedList 底層數(shù)據(jù)結(jié)構(gòu)

上圖代表了一個(gè)雙向鏈表結(jié)構(gòu),鏈表中的每個(gè)節(jié)點(diǎn)都可以向前或向后追溯顺呕,幾個(gè)概念如下:

  • 鏈表每個(gè)節(jié)點(diǎn)叫做 Node枫攀,Node 有 prev 代表前一個(gè)節(jié)點(diǎn),next 代表后一個(gè)節(jié)點(diǎn)株茶;
  • first 是雙向鏈表的頭節(jié)點(diǎn)来涨,它的前一個(gè)節(jié)點(diǎn)是 null;
  • last 是雙向鏈表的尾節(jié)點(diǎn)启盛,它的后一個(gè)節(jié)點(diǎn)是 null蹦掐;
  • 當(dāng)鏈表中沒有數(shù)據(jù)時(shí),first 和 last 是同一個(gè)節(jié)點(diǎn)僵闯,前后指向都是 null卧抗;
  • 因?yàn)槭莻€(gè)雙向鏈表,只要機(jī)器內(nèi)存足夠強(qiáng)大鳖粟,是沒有大小限制的社裆。

鏈表中的元素叫做 Node,Node 的組成部分:

private static class Node<E> {
    E item; // 節(jié)點(diǎn)值
    Node<E> next; // 指向下一個(gè)節(jié)點(diǎn)
    Node<E> prev; // 指向前一個(gè)節(jié)點(diǎn)

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

2 源碼解析

2.1 新增

追加節(jié)點(diǎn)時(shí)向图,我們可以選擇追加到鏈表頭部泳秀,還是追加到鏈表尾部,add 方法默認(rèn)是從尾部開始追加张漂,addFirst 方法是從頭部開始追加晶默,我們分別來看下兩種不同的追加方式:

2.1.1 從尾部增加(add/addLast

/**
 * Links e as last element.
 */
void linkLast(E e) {
    // 將尾節(jié)點(diǎn)暫存
    final Node<E> l = last;
    // 初始化新的節(jié)點(diǎn)谨娜。l: 新節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)航攒。e: 當(dāng)前新增節(jié)點(diǎn)值。null: 當(dāng)前新增節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 為尾節(jié)點(diǎn)賦值為當(dāng)前新增的節(jié)點(diǎn)
    last = newNode;
    // 如果尾節(jié)點(diǎn)為空(即鏈表為空)
    if (l == null)
        // 則為頭節(jié)點(diǎn)賦值為當(dāng)前新增節(jié)點(diǎn)也就是尾節(jié)點(diǎn)
        first = newNode;
    // 否則把原尾節(jié)點(diǎn) l 的下一個(gè)節(jié)點(diǎn)指向當(dāng)前新增節(jié)點(diǎn)
    else
        l.next = newNode;
    // 大小和版本增加
    size++;
    modCount++;
}

2.1.2 從頭部追加(addFirst

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
    // 將頭節(jié)點(diǎn)暫存
    final Node<E> f = first;
    // 初始化新的節(jié)點(diǎn)
    final Node<E> newNode = new Node<>(null, e, f);
    // 為頭節(jié)點(diǎn)賦值為當(dāng)前新增節(jié)點(diǎn)
    first = newNode;
    // 如果原頭節(jié)點(diǎn) f 為空(即鏈表為空)
    if (f == null)
        // 則為尾節(jié)點(diǎn)賦值為當(dāng)前新增節(jié)點(diǎn)也就是頭節(jié)點(diǎn)
        last = newNode;
    // 否則把原頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向當(dāng)前新增節(jié)點(diǎn)
    else
        f.prev = newNode;
    // 大小和版本增加
    size++;
    modCount++;
}

頭部追加節(jié)點(diǎn)和尾部追加節(jié)點(diǎn)非常類似趴梢,只是前者是移動(dòng)頭部節(jié)點(diǎn)的 prev 指向漠畜,后者是移動(dòng)尾部節(jié)點(diǎn)的 next 指向。

2.2 節(jié)點(diǎn)刪除

節(jié)點(diǎn)刪除和節(jié)點(diǎn)追加類似坞靶,我們可以選擇從頭部刪除憔狞,也可以選擇從尾部刪除,刪除操作會(huì)把節(jié)點(diǎn)的值彰阴,前后指向節(jié)點(diǎn)都置為 null瘾敢,幫助 GC 進(jìn)行回收。

2.2.1 從頭部刪除

// 從頭部刪除節(jié)點(diǎn) f 是鏈表頭部

/**
 * Unlinks non-null first node f.
 */
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 拿出頭節(jié)點(diǎn)的值
    final E element = f.item;
    // 拿出頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
    final Node<E> next = f.next;
    // 賦值頭節(jié)點(diǎn)的值和頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)為 null,幫助 GC 回收
    f.item = null;
    f.next = null; // help GC
    // 原頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn) next 為新的頭節(jié)點(diǎn)
    first = next;
    // 如果 next 為 null簇抵,表明鏈表為空
    if (next == null)
        last = null;
    else
        // 鏈表不為空庆杜,設(shè)置新的頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為 null
        next.prev = null;
    // 大小和版本更改
    size--;
    modCount++;
    // 返回被刪除節(jié)點(diǎn)的值
    return element;
}

2.2.2 從尾部刪除

// 從尾部刪除節(jié)點(diǎn) l 是鏈表尾部

/**
 * Unlinks non-null last node l.
 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    // 拿出尾節(jié)點(diǎn)的值
    final E element = l.item;
    // 拿出尾節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)
    final Node<E> prev = l.prev;
    // 賦值尾節(jié)點(diǎn)的值和尾節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為 null,幫助 GC 回收
    l.item = null;
    l.prev = null; // help GC
    // 原尾節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn) prev 為新的尾節(jié)點(diǎn)
    last = prev;
    // 如果 prev 為 null碟摆,表明鏈表為空
    if (prev == null)
        first = null;
    else
        // 鏈表為不空晃财,設(shè)置新的尾節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)為 null
        prev.next = null;
    // 大小和版本更改
    size--;
    modCount++;
    // 返回被刪除節(jié)點(diǎn)的值
    return element;
}

從源碼中我們可以了解到,鏈表結(jié)構(gòu)的節(jié)點(diǎn)新增典蜕、刪除都非常簡單断盛,僅僅把前后節(jié)點(diǎn)的指向修改而已,所以 LinkedList 新增和刪除速度很快愉舔。

2.3 節(jié)點(diǎn)查詢

鏈表查詢某一個(gè)節(jié)點(diǎn)是比較慢的钢猛,需要挨個(gè)循環(huán)查找才行,源碼如下:

// 根據(jù)鏈表索引位置查詢節(jié)點(diǎn)

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    // 如果 index 處于隊(duì)列的前半部分轩缤,則從頭開始查找厢洞,size >> 1 是 size 除以 2 的意思
    if (index < (size >> 1)) {
        Node<E> x = first;
        // for 循環(huán)到 index 的前一個(gè) node 停止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } 
    // 如果 index 處于隊(duì)列的后半部分,則從尾部開始查找
    else {
        Node<E> x = last;
        // for 循環(huán)到 index 的后一個(gè) node 停止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

從源碼中我們發(fā)現(xiàn)典奉,LinkedList 并沒有采用從頭循環(huán)到尾的做法躺翻,而是采取了簡單二分法,首先看看 index 是在鏈表的前半部分卫玖,還是后半部分公你。如果是前半部分,就從頭開始尋找假瞬,反之亦然陕靠。通過這種方式,使循環(huán)的次數(shù)至少降低了一半脱茉,提高了查找的性能剪芥,這種思想值得我們借鑒。

2.4 方法對(duì)比

LinkedList 實(shí)現(xiàn)了 Queue 接口琴许,在新增税肪、刪除、查詢等方面增加了很多新的方法榜田,這些方法平時(shí)特別容易混淆益兄,在鏈表為空的情況下,返回值也不太一樣箭券,如下:

|方法含義|返回異常|返回特殊值|底層實(shí)現(xiàn)|
|----|----|----|----|----|
|新增(尾部)|add(e)|offer(e)|底層實(shí)現(xiàn)相同|
|刪除(頭節(jié)點(diǎn))|remove()|poll()|鏈表為空時(shí)净捅,remove 會(huì)拋出異常,poll 返回 null|
|查找(頭節(jié)點(diǎn))|element()|peek()|鏈表為空時(shí)辩块,element 會(huì)拋出異常蛔六,peek 返回 null|

PS:Queue 接口注釋建議 add 方法操作失敗時(shí)拋出異常荆永,但 LinkedList 實(shí)現(xiàn)的 add 方法一直返回 true。
LinkedList 也實(shí)現(xiàn) Deque 接口国章,對(duì)新增屁魏、刪除和查找都提供從頭開始,還是從尾開始兩種方向的方法捉腥,比如 remove 方法氓拼,Deque 提供了 removeFirst 和 removeLast 兩種方向的使用方法,但當(dāng)鏈表為空時(shí)的表現(xiàn)都和 remove 方法一樣抵碟,都會(huì)拋出異常桃漾。

2.5 迭代器

因?yàn)?LinkedList 要實(shí)現(xiàn)雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行拟逮,因?yàn)?Iterator 只支持從頭到尾的訪問撬统。Java 新增了一個(gè)迭代接口,叫做:ListIterator敦迄,這個(gè)接口提供了向前和向后的迭代方法恋追,如下:

迭代順序 相關(guān)方法
從頭到尾迭代 hasNext, next, nextIndex
從尾到頭迭代 hasProvious, previous, previousIndex

LinkedList 實(shí)現(xiàn)了 ListIterator 接口,源碼如圖:

// 雙向迭代器
private class ListItr implements ListIterator<E> {
    // 按照命名罚屋,意思為上一次執(zhí)行 next() 或者 previous() 方法返回的節(jié)點(diǎn)
    private Node<E> lastReturned;
    // 下一個(gè)節(jié)點(diǎn)
    private Node<E> next;
    // 下一個(gè)節(jié)點(diǎn)的位置
    private int nextIndex;
    // 期望版本號(hào)
    private int expectedModCount = modCount;
    ........
}

從頭到尾方向的迭代

// 判斷有沒有下一個(gè)元素
public boolean hasNext() {
    // 下一個(gè)節(jié)點(diǎn)的索引小于鏈表的大小苦囱,就有
    return nextIndex < size;
}

// 取下一個(gè)元素
public E next() {
    // 檢查版本號(hào)有無變化
    checkForComodification();
    // 再次檢查是否還有下一個(gè)元素
    if (!hasNext())
        throw new NoSuchElementException();

    // 賦值 lastReturned,首次執(zhí)行 next() 方法時(shí) 賦值為 null
    lastReturned = next;
    // next 是下一個(gè)節(jié)點(diǎn)脾猛,
    next = next.next;
    // 索引變化
    nextIndex++;
    return lastReturned.item;
}

從尾到頭方向的迭代

// 判斷是否還有上一個(gè)節(jié)點(diǎn)撕彤,我們知道最小索引為 0, 如果 nextIndex 大于0則表示還有上一個(gè)元素
public boolean hasPrevious() {
    return nextIndex > 0;
}

public E previous() {
    // 檢查版本號(hào)有無變化
    checkForComodification();
    // 再次檢查是否還有上一個(gè)元素
    if (!hasPrevious())
        throw new NoSuchElementException();

    // 如果 next 為null, 則表示當(dāng)前是從尾到頭的第一次迭代猛拴,則賦值 next 為 last羹铅,
    // 否則賦值 next 為 next 的前一個(gè)元素 prev
    lastReturned = next = (next == null) ? last : next.prev;
    // 索引變化
    nextIndex--;
    return lastReturned.item;
}

迭代器刪除

LinkedList 在刪除元素時(shí),也推薦通過迭代器進(jìn)行刪除愉昆,源碼如下:

public void remove() {
    // 檢查版本號(hào)有無變化
    checkForComodification();
    // 首先 lastReturned 是本次迭代的值
    // 如果 lastReturned 為 null职员,表示未執(zhí)行 next() 或 pervious() 方法,那刪除直接報(bào)錯(cuò)
    // 如果 lastReturned 不為 null 則繼續(xù)執(zhí)行
    if (lastReturned == null)
        throw new IllegalStateException();

    // 將本次迭代要?jiǎng)h除的元素 lastReturned 的下一個(gè)元素緩存
    Node<E> lastNext = lastReturned.next;
    // 刪除 lastReturned
    unlink(lastReturned);
    // 如果是從尾到頭的第一次迭代跛溉,則 lastReturned = next = last. 參考上面方法 previous()
    if (next == lastReturned)
        // 這個(gè)時(shí)候 lastReturned 是尾節(jié)點(diǎn)焊切,lastNext 是 null,所以 next 也是 null倒谷,這樣在執(zhí)行 previous() 方法判斷 next 是否為null 時(shí)蛛蒙,發(fā)現(xiàn) next 是null糙箍,就會(huì)把尾結(jié)點(diǎn) last 賦值給 next
        next = lastNext;
    else
        // 反之索引遞減
        nextIndex--;
    lastReturned = null;
    expectedModCount++;
}

總結(jié)

LinkedList 適用于要求有順序渤愁、并且會(huì)按照順序進(jìn)行迭代的場(chǎng)景,主要是依賴于底層的鏈表結(jié)構(gòu)深夯。

------------------------------------- END -------------------------------------

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抖格,一起剝皮案震驚了整個(gè)濱河市诺苹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雹拄,老刑警劉巖收奔,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異滓玖,居然都是意外死亡坪哄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門势篡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翩肌,“玉大人,你說我怎么就攤上這事禁悠∧罴溃” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵碍侦,是天一觀的道長粱坤。 經(jīng)常有香客問我,道長瓷产,這世上最難降的妖魔是什么站玄? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮濒旦,結(jié)果婚禮上蜒什,老公的妹妹穿的比我還像新娘。我一直安慰自己疤估,他們只是感情好灾常,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铃拇,像睡著了一般钞瀑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慷荔,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天雕什,我揣著相機(jī)與錄音,去河邊找鬼显晶。 笑死贷岸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的磷雇。 我是一名探鬼主播偿警,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼唯笙!你這毒婦竟也來了螟蒸?” 一聲冷哼從身側(cè)響起盒使,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎七嫌,沒想到半個(gè)月后少办,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诵原,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年英妓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绍赛。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鞋拟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惹资,到底是詐尸還是另有隱情贺纲,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布褪测,位于F島的核電站猴誊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏侮措。R本人自食惡果不足惜懈叹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望分扎。 院中可真熱鬧澄成,春花似錦、人聲如沸畏吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菲饼。三九已至肾砂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宏悦,已是汗流浹背镐确。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饼煞,地道東北人源葫。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像砖瞧,于是被迫代替她去往敵國和親息堂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容