1. 程式人生 > 實用技巧 >面試老被問LinkedList原始碼?看看阿里技術官是怎麼深度剖析的吧!

面試老被問LinkedList原始碼?看看阿里技術官是怎麼深度剖析的吧!

前言

LinkedList底層是基於雙向連結串列,連結串列在記憶體中不是連續的,而是通過引用來關聯所有的元素,所以連結串列的優點在於新增和刪除元素比較快,因為只是移動指標,並且不需要判斷是否需要擴容,缺點是查詢和遍歷效率比較低。下面會給大家詳細的剖析一下底層原始碼!

結構

LinkedList 繼承關係,核心成員變數,主要建構函式:

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

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

    	//------------------------成員變數-------------------------------------

    	transient int size = 0;

    	// 記錄頭結點,它的前一個結點=null
        transient Node<E> first;

    	// 記錄尾結點,它的後一個結點=null
    	// 當 first = last = null時表示連結串列為空
    	// 當 first = last != null時表示只有一個節點
        transient Node<E> last;

        //--------------------------構造方法-------------------------------------

        public LinkedList() {
        }

         public LinkedList(Collection<? extends E> c) {
            this();
            addAll(c);
        }

        // ........
    }

方法解析&api

追加

追加節點時,我們可以選擇追加到連結串列頭部,還是追加到連結串列尾部,add 方法預設是從尾部開始追加,addFirst 方法是從頭部開始追加,我們分別來看下兩種不同的追加方式:

-add()

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

--linkLast()

    /**
     * 尾插
     * newNode.pre = last
     * last.next = newNode   注:考慮last=null情況(連結串列為空,這時僅更新頭結點即可)
     * last = newNode
    */
    void linkLast(E e) {
        // 把尾節點資料暫存,為last.next做準備,其實改變一下順序就可以不要這個l了
        final Node<E> l = last;

        final Node<E> newNode = new Node<>(l, e, null); // 1
        last = newNode; // 2

        // 空連結串列,l=null,l.next報空指標
        if (l == null)
            first = newNode;
        else
            l.next = newNode; // 3

        // size和版本更改
        size++;
        modCount++;
    }

-addFirst()

    public void addFirst(E e) {
            linkFirst(e);
    }

--linkFirst()

    /**
    * 頭插
    * newNode.next = first;
    * first.prev = newNode;  注:考慮first=null(連結串列為空,只用更新last即可)
    * first = newNode;
    */
    private void linkFirst(E e) {
        // 頭節點賦值給臨時變數
        final Node<E> f = first;

        final Node<E> newNode = new Node<>(null, e, f); // 1

        first = newNode;  // 2

        // 連結串列為空,f=null, f.prev報空指標
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;  // 3

        // 更新size和版本號
        size++;
        modCount++;
    }

刪除

節點刪除的方式和追加類似,我們可以刪除指定元素,或者從頭部(尾部)刪除,刪除操作會把節點的值,前後指向節點都置為 null,幫助 GC 進行回收

-remove()

    /**
    *刪除指定元素;找到要刪除的節點
    *注:只有連結串列有這個節點且成功刪除才返回true
    */
    public boolean remove(Object o) { 
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    // null用 == 判斷
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    // 呼叫equals判斷,若傳入的類無equals需要重寫
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;  // 連結串列無要刪除元素,或連結串列為空
    }

注:remove還可以根據索引刪除

    public E remove(int index) { 
            checkElementIndex(index); // 連結串列為空,丟擲異常
            return unlink(node(index));
    }

    /**
    * 執行刪除
    * x.prev.next = x.next        注:考慮x.prev=null(x是first,直接更新first)
    * x.next.prev = x.prev.prev   注:考慮x.next=null(x是last,直接更新last)
    */
    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;

         	// 如果prev=null,則當前節點為頭結點
            if (prev == null) {
                // 直接將頭結點賦成next
                first = next;
            } else {
                prev.next = next; // 1
                x.prev = null; // 幫助 GC 回收該節點
            }

         	// 如果next=null,則當前節點為尾結點
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev; // 2
                x.next = null; // 幫助 GC 回收該節點
            }

            x.item = null; // 幫助 GC 回收該節點

         	// 修改size及版本
            size--;
            modCount++;

            return element;
        }

-remove()

    /**
    *刪除頭節點,佇列為空時丟擲異常
    */
    public E remove() {
            return removeFirst();
    }

-removeFirst()

    /**
    *刪除頭節點
    */
    public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
     }

--unLinkFirst()

    /**
    * 執行刪除頭節點
    * first.next.pre = null;  注:考慮first=null(連結串列為空), first.next=null(尾結點,即連結串列僅一個節點)
    * first = first.next;
    */
    private E unlinkFirst(Node<E> f) {

        final E element = f.item; // 拿出頭節點的值,作為方法的返回值
        final Node<E> next = f.next; // 拿出頭節點的下一個節點

        //幫助 GC 回收頭節點
        f.item = null;
        f.next = null;

        first = next;  // 1

        // next為空表示連結串列只有一個節點
        if (next == null)
            last = null;
        else
            next.prev = null; // 2

        //修改連結串列大小和版本
        size--;
        modCount++;
        return element;
    }

從原始碼中我們可以瞭解到,連結串列結構的節點新增、刪除都非常簡單,僅僅把前後節點的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。

查詢

連結串列查詢某一個節點是比較慢的,需要挨個迴圈查詢才行,我們看看 LinkedList 的原始碼是如何尋找節點的

-get()

    /**
    *根據索引進行查詢
    */
    public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
    }

--node()

    Node<E> node(int index) {
        // 如果 index 處於佇列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
        if (index < (size >> 1)) {
            // 取頭節點
            Node<E> x = first;
            // 直到 for 迴圈到 index 的前一個 node 停止
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {// 如果 index 處於佇列的後半部分,從尾開始找
            // 取尾結點
            Node<E> x = last;
            // 直到 for 迴圈到 index 的後一個 node 停止
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

從原始碼中我們可以發現,LinkedList 並沒有採用從頭迴圈到尾的做法,而是採取了簡單二分法,首先看看 index 是在連結串列的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使迴圈的次數至少降低了一半,提高了查詢的效能,這種思想值得我們借鑑

迭代器

因為 LinkedList 要實現雙向的迭代訪問,所以使用 Iterator 介面肯定不行了,因為 Iterator 只支援從頭到尾的訪問。Java 新增了一個迭代介面,叫做:ListIterator,這個介面提供了向前和向後的迭代方法,如下所示:

迭代順序 方法
從尾到頭迭代方法 hasPrevious、previous、previousIndex
從頭到尾迭代方法 hasNext、next、nextIndex

-listIterator()

    /**
    *從指定節點開始迭代,可前可後
    */
    public ListIterator<E> listIterator(int index) {
        checkPositionIndex(index);
        return new ListItr(index);
    }

    /**
    *ListItr,雙向迭代器
    */
    private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;//上一次執行 next() 或者 previos() 方法時的節點位置
        private Node<E> next;//下一個節點
        private int nextIndex;//下一個節點的位置
        //expectedModCount:期望版本號;modCount:目前最新版本號
        private int expectedModCount = modCount;

        ListItr(int index) {
              // assert isPositionIndex(index);
              next = (index == size) ? null : node(index);
              nextIndex = index;
        }
    }

--hasNext()

從前向後迭代

    // 判斷還有沒有下一個元素,還是通過index和size控制
    public boolean hasNext() {
        return nextIndex < size;// 下一個節點的索引小於連結串列的大小,就有
    }

---next()

    // 取下一個元素,並後移
    public E next() {
        //檢查期望版本號有無發生變化
        checkForComodification();
        if (!hasNext())//再次檢查
            throw new NoSuchElementException();
        // next 是當前節點,在上一次執行 next() 方法時被賦值的。
        // 第一次執行時,是在初始化迭代器的時候,next 被賦值的
        lastReturned = next;
        // next 是下一個節點了,為下次迭代做準備
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }

--hasPrevious()

從後向前迭代

    // 如果上次節點索引位置大於 0,就還有節點可以迭代
    public boolean hasPrevious() {
        return nextIndex > 0;
    }

---previous()

    public E previous() {
        checkForComodification();
        if (!hasPrevious())
            throw new NoSuchElementException();
        // next 為空場景:1:說明是第一次迭代,取尾節點(last);2:上一次操作把尾節點刪除掉了
        // next 不為空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev)
        lastReturned = next = (next == null) ? last : next.prev;
        // 索引位置變化
        nextIndex--;
        return lastReturned.item;
    }

----remove()

    /**
    *迭代時,刪除當前元素
    */
    public void remove() {
        checkForComodification();
        // lastReturned 是本次迭代需要刪除的值,分以下空和非空兩種情況:
        // lastReturned 為空,說明呼叫者沒有主動執行過 next() 或者 previos(),直接報錯
        // lastReturned 不為空,是在上次執行 next() 或者 previos()方法時賦的值
        if (lastReturned == null)
            throw new IllegalStateException();
        Node<E> lastNext = lastReturned.next;
        //刪除當前節點
        unlink(lastReturned);
        // next == lastReturned 的場景分析:從尾到頭遞迴順序,並且是第一次迭代,並且要刪除最後一個元素的情況
        // 這種情況下,previous()方法裡面設定了 lastReturned=next=last,所以 next 和l astReturned 會相等
        if (next == lastReturned)
            // 這時候 lastReturned 是尾節點,lastNext 是 null,所以 next 也是 null,這樣在 previous() 執行		 // 時,發現 next 是 null,就會把尾節點賦值給 next
            next = lastNext;
        else
            nextIndex--;
        lastReturned = null;
        expectedModCount++;
    }

Queue的實現

LinkedList 實現了 Queue 介面,在新增、刪除、查詢等方面增加了很多新的方法,這些方法在平時特別容易混淆,在連結串列為空的情況下,返回值也不太一樣,下面列一個表格,方便大家記錄:

PS:Queue 介面註釋建議 add 方法操作失敗時丟擲異常,但 LinkedList 實現的 add 方法一直返回 true。
LinkedList 也實現了 Deque 介面,對新增、刪除和查詢都提供從頭開始,還是從尾開始兩種方向的方法,比如 remove 方法,Deque 提供了 removeFirst 和 removeLast 兩種方向的使用方式,但當連結串列為空時的表現都和 remove 方法一樣,都會丟擲異常。

最後

感謝你看到這裡,文章有什麼不足還請指正,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!