初探Java源碼之LinkedList

上篇文章我們分析了常見的ArrayList源碼,它的內部是由一個數(shù)組來實現(xiàn)的。那么今天,我們來分析另一個常見的類LinkedList。本文分析都來自Java8。

(ps:這段話寫自寫完本文記錄后添加。個人感想為已經寫成了介紹鏈表)


類說明

不多廢話,首先我們來看一下這個類。

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這個類名我們就猜出,這個List內部可能是由鏈表來實現(xiàn),我們后面來驗證一下。它實現(xiàn)了Deque接口,因此可以知道,LinkedList也可以作為隊列來進行使用。同時也實現(xiàn)了Serializable接口,說明可以對它進行序列化,反序列化來傳遞,獲取數(shù)據(jù)等等。
我們再來看看LinkedList的成員變量。首先是一個int類型的size,這個我們在ArrayList中也介紹過。這個是一個表明List有多少個元素。然后是一個Node類型的first變量,從注釋我們看出指向頭結點的變量(用C,C++解釋稱之為指針,也許用C語言的方式更好解釋)。后面的last變量也很好理解,它指向最后結點。我們跟進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的內部類,還是比較簡單的。首先一個泛型的item變量,用來存儲數(shù)據(jù),然后有一個指向下一個結點的next指針,前一個結點的prev指針。

至此我們其實就可以得出結論,LinkedList果然名不虛傳,內部就是由鏈表的形式實現(xiàn)。它并沒有用數(shù)組來存儲數(shù)據(jù)元素,而是由一個個Node類型結點來儲存數(shù)據(jù),然后每個Node結點通過指向前后結點的next和prev指針將整個List串聯(lián)起來。我們來畫張簡單圖來看看ArrayList和LinkedList的基本區(qū)別:

ArrayList和LinkedList區(qū)別

正是這樣的區(qū)別,從而導致了兩者不同的效率問題。如果我們對一個ArrayList頻繁的增加數(shù)據(jù),那么內部的數(shù)組就會不斷擴容創(chuàng)建新的數(shù)組然后把舊數(shù)據(jù)復制到新數(shù)組返回。插入數(shù)據(jù)或者刪除其中一個數(shù)據(jù),又要把所有數(shù)據(jù)后移或者前移。這樣的操作效率很低下的,為了一個數(shù)據(jù)處理了其他數(shù)據(jù)。

而LinkedList不同,插入只需要將要插入位置的前結點的next指針指向新的數(shù)據(jù)結點(如下圖插入工作圖的2),將插入結點的prev指向前指針(如1處),將next指向下一個結點(如3處),將之前的后結點的prev指針指向插入指針(如4處)。刪除也是如下刪除工作圖,就不再講述。這樣的話只需要幾步操作就完成了處理,無需移動大量數(shù)據(jù)。效率非常高。(下面兩幅圖均來自網上大神的鏈表介紹文章http://www.cnblogs.com/bb3q/p/4490589.html)無恥的借用一下下~~~

插入工作圖

刪除工作圖

那既然如此ArrayList豈不是一無是處了?為啥還要用呢?其實并不是,鏈表既然是用指針的方式連起來,那么意味著我們尋找某個結點就需要從頭開始遍歷,直到找到這個元素為止,并不能提供隨機訪問。例如我們想找數(shù)組的第五個元素,那么只需xxx[4]即可得到,但是鏈表不行,只能從頭開始遍歷到第五個元素才行。因此在開發(fā)中如果需要對list頻繁的添加,刪除,插入,那么用LinkedList是很好的。但是數(shù)據(jù)量不大,需要經常查詢數(shù)據(jù)的時候,ArrayList更適合。

至此我們就將LinkedList的類和成員變量介紹完了,也分析了和ArrayList的區(qū)別。接下來我們來看看構造方法:

    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);
    }

第一個構造方法是空實現(xiàn),因為內部是鏈表,無需像ArrayLsi一樣t實例化數(shù)組。

我們來看看第二個構造方法,傳入?yún)?shù)為一個集合,主要是調用了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;
    }

第一個addAll()方法將size傳入,其實就是從原有List的末尾開始添加傳入的數(shù)據(jù),而這里size為0。所以就是從頭開始添加數(shù)據(jù)。我們仔細分析第二個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;
        }

然后將傳入的集合首先轉換為數(shù)組。然后判斷數(shù)組長度是否為0,如果是0,表示沒有添加的數(shù)據(jù),直接返回。

如果不是,那么定義兩個結點succ和pred。個人理解succ表示要插入的index下標的結點,pred表示succ結點的前結點。然后判斷index是否等于size,而我們在構造方法中傳入的index就等于size,所以表示從鏈表的末尾開始添加數(shù)據(jù)。即succ為null(因為下標從0開始,所以最后一個數(shù)據(jù)下標為size - 1),pred為last最后一個結點。如果index不等于size,那么找到index處的結點賦值給succ,pred等于succ的前結點。

 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;
        }

到這里就已經把預處理工作完成了。接下來就是添加數(shù)據(jù)過程。首先是一個for循環(huán)遍歷a數(shù)組。每個對象(也就是數(shù)據(jù))都生成一個結點。然后將新結點的前指針指向pred結點。如果pred為空(這種情況就是鏈表為空,沒有一個結點),那么這個新結點就是鏈表的表頭。如果pred不為空,那么將pred的后指針指向這個新結點。這樣就將pred結點和新結點串聯(lián)起來。然后pred往后移一個結點,指向新結點。這樣就不斷的將所有新數(shù)據(jù)插入到指點位置往后。

 if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

for循環(huán)后,會判斷succ是否為空(即我們插入位置的舊結點是否為空)。如果為空,表明我們是插入到鏈表的末尾的,那么就無需將舊結點的后結點的prev指針修改(因為根本沒后結點),直接將last指向pred(因為在for循環(huán)中pred會指向新數(shù)據(jù)集合的最后一個數(shù)據(jù))即可。如果succ不為空,表示我們是在鏈表的中間插入數(shù)據(jù)的。因此就要將新數(shù)據(jù)集合的最后一個數(shù)據(jù)結點的next后指針指向我們插入位置的結點。然后將未插入前的index處的結點的前結點修改為指向新插入的最后一個數(shù)據(jù)結點即pred。

        size += numNew;
        modCount++;
        return true;

最后將數(shù)據(jù)數(shù)量加上新插入的數(shù)據(jù)數(shù)量,修改數(shù)據(jù)結構次數(shù)自加1即可。

至此我們就已經將構造方法分析完畢,可以說就是最常見的數(shù)據(jù)結構中雙向鏈表的操作方法。接下來我們繼續(xù)分析一些常見的方法。


add()

我們來看看最常用的幾個add()方法,首先是第一個:

  public boolean add(E e) {
        linkLast(e);
        return true;
    }

我們看到就是調用了linkLast()方法,我們跟進去看看:

 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++;
    }

我們來看看這個方法,首先定義了兩個常量l和newNode結點。分別表示鏈表中最后一個結點和一個封裝了新數(shù)據(jù)的新結點。新結點的前指針指向l。然后將last指針指向新結點。然后判斷如果l結點(即之前的最后一個結點)是空,表明鏈表之前沒有數(shù)據(jù),加入的新數(shù)據(jù)將會稱為第一個結點。因此將表頭first指針指向新結點。如果l不為空。則將之前最后一個結點的后指針指向新結點,新結點指向l。然后size自加1,modCount自加1表示修改了一次鏈表。這樣就將新數(shù)據(jù)插入到了鏈表的末尾。

第二個add()方法:

public void add(int index, E element) {
        checkPositionIndex(index);

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

首先是調用checkPositionIndex()方法檢查index是否大于0并小于size。
然后如果index等于size,表示我們要將數(shù)據(jù)插入到鏈表的末尾,調用linkLast()方法來插入數(shù)據(jù),上面分析了該方法這里就不在贅述。如果index不等于size,那么表示是在鏈表的中間插入數(shù)據(jù),那么調用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++;
    }

可以看到其實也是差不多的,首先定義兩個常量Node變量pred和newNode用來表示我們要插入的結點的前結點和一個要插入的新結點,這個新結點的前指針指向pred,后指針指向插入位置的舊結點。然后將要插入位置的舊結點的prev前指針指向新結點。如果pred為空,表示我們要插入的位置是表頭,直接將表頭指針first指向新結點。如果不為空,那么將pred的后指針next指向新結點。

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;
    }

我們來看看第一個remove()方法,傳入的參數(shù)表示是一個數(shù)據(jù)。雖然要首先判斷我們傳入的數(shù)據(jù)是否為空來分開操作。但是其實都是一樣的做遍歷然后刪除結點。如果是空,那么就要進行for循環(huán)從表頭開始遍歷,有人可能會好奇,傳入為空的話為什么還要找,沒有意義呀?這里我的想法是鏈表是一種數(shù)據(jù)結構,它是以結點為基礎存在的,不關心數(shù)據(jù)。所以有可能某個結點不為空,但是結點封裝的數(shù)據(jù)為空。因此,在某些特殊場景下萬一有些人就是要存空數(shù)據(jù)呢?我們重點看看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;
    }

首先將要刪除結點的數(shù)據(jù)取出。然后得到刪除結點的前結點prev,后結點next。然后如果前結點為空,表明刪除結點為頭結點,因此直接將表頭first指針指向刪除結點的后結點。否則的話將prev的next指針直接跳過刪除結點指向next結點。然后將輸出結點的prev指針置空。后面的next判空也是同理。最后將刪除結點的數(shù)據(jù)置空,鏈表數(shù)量自減1,modCount自加1,然后刪除結點的數(shù)據(jù)。至此,結點刪除就已經完成。
接下來我們來看看第二個remove()方法:

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

這個更簡單,移除指定的結點。還是首先檢查index的合法性,然后得到指定位置的結點調用unlink()方法移除即可。上面已分析unlink()方法,就不再贅述。
第三個remove()方法:

 public E remove() {
        return removeFirst();
    }

說實話這個remove()方法我不是很理解為什么這些寫,就是調用removeFirst()方法來移除表頭結點。百思不得其解為什么要這么設計,而且remove()這個方法名也根本看不出是移除表頭的意思。也沒有什么可以分析的,就是一個簡單的移除表頭結點。

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就是將鏈表清空。所以直接一個for循環(huán),從表頭開始一直到表尾。將每個結點的數(shù)據(jù)直接置空即可。最后將表頭,表尾first,last置空。size置為0即可。

get()

  public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

get()方法也很常用,我們來看看,首先調用checkElementIndex()方法檢查index是否大于0并小于size。然后調用了node()方法來獲取指定位置的結點。我們去看看:

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;
        }
    }

我們看到,這里還是用了一點小技巧。首先判斷我們想要的位置是否是在鏈表的前半部分還是后半部分(size >> 1 表示除以2),如果在前半部分那么就從表頭開始遍歷,在后半部分就從表尾開始往前遍歷。知道拿到目標結點即可。

set()

 public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

首先也是調用checkElementIndex()方法檢查index是否大于0并小于size。然后也是調用node()方法獲取指定位置的結點。然后將指定結點的item數(shù)據(jù)置為新值,返回舊值即可。

至此我們常見的LinkedList的方法源碼分析就已經完了,其他的一些方法要么不怎么用,要么非常簡單只有一兩行簡單代碼甚至就是調用這些常見方法,讀者一跟進去就能明白,這里就不再深究。


最后我們再來總結一下:

首先LinkedList內部是由雙向鏈表來實現(xiàn)的。我們儲存的每一個數(shù)據(jù)都會被封裝在一個數(shù)據(jù)結點之中。而結點瘋轉了指向前結點的指針,數(shù)據(jù),指向后結點的指針。依靠這些數(shù)據(jù)結點實現(xiàn)雙向鏈表。
既然是鏈表,那么優(yōu)點就是添加,插入,刪除數(shù)據(jù)效率比數(shù)組高很多。因為在插入或者刪除某個數(shù)據(jù)時,只需對要刪除結點,前結點,后結點進行操作,無需像數(shù)組一樣將后續(xù)數(shù)據(jù)全部前移或者后移。但是由此也看出缺點,因為鏈表并不是連續(xù)的空間儲存,也沒有什么下標進行記錄位置。因此要尋找某個數(shù)據(jù)時只能進行遍歷,而不像數(shù)組一樣可以隨機查找。如果我們在實際開發(fā)中我們需要對某個List進行頻繁的插入,刪除,而且數(shù)據(jù)量又特別大的時候。可以考慮使用LinkedList。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容