LinkedList實現原理分析(Java源碼剖析)

  • 本文對LinkedList的實現討論都基于JDK8版本

Java中的LinkedList類實現了List接口和Deque接口,是一種鏈表類型的數據結構,支持高效的插入和刪除操作,同時也實現了Deque接口,使得LinkedList類也具有隊列的特性。LinkedList類的底層實現的數據結構是一個雙端的鏈表。

LinkedList類中有一個內部私有類Node,這個類就代表雙端鏈表的節點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的實現中多次調用。

下圖是LinkedList內部結構的可視化,能夠幫我們更好的理解LinkedList內部的結構。

image.png

雙端鏈表由node組成,每個節點有兩個reference指向前驅節點和后繼結點,第一個節點的前驅節點為null,最后一個節點的后繼節點為null。

LinkedList類有很多方法供我們調用。我們不會一一介紹,本文會詳細介紹其中幾個最核心最基本的方法,LinkedList的創建添加和刪除基本都和這幾個操作有關。

  • linkFirst() method
    首先我們介紹第一個方法,linkFirst(),顧名思義,這個方法是插入第一個節點,我們先直接上代碼,看看它的具體實現
/**
     * Links e as first element.
     */
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

我們發現出現了兩個變量,first和last這兩個變量是LinkedList的成員變量,分別指向頭結點和尾節點。他們是如下定義的:

/**
     * 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;

我們可以看到注釋中的內容。first和last需要維持一個不變量,也就是first和last始終都要維持兩種狀態:
首先,如果雙端鏈表為空的時候,兩個都必須為null
如果鏈表不為空,那么first的前驅節點一定是null,first的item一定不為null,同理,last的后繼節點一定是null,last的item一定不為null。

知道了first和last之后,我們就可以開始分析linkFirst的代碼了。
linkFirst的作用就是在first節點的前面插入一個節點,插入完之后,還要更新first節點為新插入的節點,并且同時維持last節點的不變量。

我們開始分析代碼,首先用f來臨時保存未插入前的first節點,然后調用的node的構造函數新建一個值為e的新節點,這個節點插入之后將作為first節點,所以新節點的前驅節點為null,值為e,后繼節點是f,也就是未插入前的first節點。
然后就是維持不變量,首先第一種情況,如果f==null,那就說明插入之前,鏈表是空的,那么新插入的節點不僅是first節點還是last節點,所以我們要更新last節點的狀態,也就是last現在要指向新插入的newNode。
如果f!=null那么就說明last節點不變,但是要更新f的前驅節點為newNode,維持first節點的不變量。
最后size加一就完成了操作。

  • linkLast() method
    分析了linkFirst方法,對于 linkLast()的代碼就很容易理解了,只不過是變成了插入到last節點的后面。我們直接看代碼
/**
     * Links e as last element.
     */
    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是實現了Deque接口的。
從源碼中也可以看出,addfirst和addLast這兩個方法內部就是直接調用了linkFirst和LinkLast

/**
     * Inserts the specified element at the beginning of this list.
     *
     * @param e the element to add
     */
    public void addFirst(E e) {
        linkFirst(e);
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     */
    public void addLast(E e) {
        linkLast(e);
    }
  • linkBefore(E e, Node<E> succ)
    下面我們看一個linkBefore方法,從名字可以看出這個方法是在給定的節點前插入一個節點,可以說是linkFirst和linkLast方法的通用版。
/**
     * Inserts element e before non-null 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++;
    }

我們可以看到代碼的實現原理基本和前面的兩個方法一致,這里是假設插入的這個節點的位置是非空的。

  • add(int index, E element)
    下面我們看add方法,這個方法就是最常用的,在指定下標插入一個節點。我們先來看下源碼的實現,很簡單
/**
     * Inserts the specified element at the specified position in this list.
     * Shifts the element currently at that position (if any) and any
     * subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

首先判斷給定的index是不是合法的,然后如果index==size,就說明要插入成為最后一個節點,直接調用linklast方法,否則就調用linkBefore方法,我們知道linkBefore需要給定兩個參數,一個插入節點的值,一個指定的node,所以我們又調用了Node(index)去找到index的那個node。
我們看一下Node<E> node(int index)方法,這個方法就是找到給定index的node并返回,類似于數組的隨機讀取,但由于這里是鏈表,所以要進行查找

/**
     * Returns the (non-null) Node at the specified element index.
     */
    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的實現并不是像我們想象的那樣直接就線性從頭查找,而是折半查找,有一個小優化,先判斷index在前半段還是后半段,如果在前半段就從頭開始找,如果在后半段就從后開始找,這樣最壞情況也只要找一半就可以了。

LinkedList的源碼實現并不復雜,我們只介紹這幾個方法,相信你一定對于它的內部實現原理有了一定的了解,并且也學習到了優秀的代碼書寫風格和優化。
對于remove操作,有興趣的讀者可以自行研究代碼,它類似于add操作,也是基于三個基本方法來實現的。

  • unlinkFirst(Node<E> f)
  • unlinkLast(Node<E> l)
  • unlink(Node<E> x)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • 一、基本數據類型 注釋 單行注釋:// 區域注釋:/* */ 文檔注釋:/** */ 數值 對于byte類型而言...
    龍貓小爺閱讀 4,278評論 0 16
  • 一.線性表 定義:零個或者多個元素的有限序列。也就是說它得滿足以下幾個條件:??①該序列的數據元素是有限的。??②...
    Geeks_Liu閱讀 2,703評論 1 12
  • Java Collection框架 - LinkedList 基于jdk1.8 簡介 LinkedList采用鏈表...
    xiedacon閱讀 954評論 0 2
  • 鏈表 概念 說到鏈表,coder們都不會陌生,在日常開發中或多或少都會用到它。它是鏈式存儲的線性表,簡稱鏈表。鏈表...
    扈扈哈嘿閱讀 2,084評論 0 5
  • 煙雨環繞諸山,處處山峰仿佛化成茫茫大海中青蒼之舟,山腳居紅塵,山頂位于虛空中。這里是霧中山。 霧中山上,初春時分,...
    凈松閱讀 615評論 3 2