Java小白集合源碼的學習系列:LinkedList


LinkedList 源碼學習

前文傳送門Java小白集合源碼的學習系列:ArrayList

本篇為集合源碼學習系列的LinkedList學習部分,如有敘述不當之處,還望評論區批評指正!

LinkedList繼承體系

LinkedList和ArrayList一樣,都實現了List接口,都代表着列表結構,都有着類似的add,remove,clear等操作。與ArrayList不同的是,LinkedList底層基於雙向鏈表允許不連續地址的存儲,通過節點之間的相互引用建立聯系,通過節點存儲數據。

LinkedList核心源碼

既然是基於節點的,那么我們來看看節點在LinkedList中是怎樣的存在:

    //Node作為LinkedList的靜態內部類
    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;
        }
    }

我們發現,Node作為其內部類,擁有三個屬性,一個是用來指向前一節點的指針prev,一個是指向后一節點的指針next,還有存儲的元素值item
我們來看看LinkedList的幾個基本屬性:

    /*用transient關鍵字標記的成員變量不參與序列化過程*/
    transient int size = 0;//記錄節點個數

    /**
     * first是指向第一個節點的指針。永遠只有下面兩種情況:
     * 1、鏈表為空,此時first和last同時為空。
     * 2、鏈表不為空,此時第一個節點不為空,第一個節點的prev指針指向空
     */
    transient Node<E> first;

    /**
     * last是指向最后一個節點的指針,同樣地,也只有兩種情況:
     * 1、鏈表為空,first和last同時為空
     * 2、鏈表不為空,此時最后一個節點不為空,其next指向空          
     */
    transient Node<E> last;

    //需要注意的是,當first和last指向同一節點時,表明鏈表中只有一個節點。

了解基本屬性之后,我們看看它的構造方法,由於不必在乎它存儲的位置,它的構造器也是相當簡單的:

    //創建一個空鏈表
    public LinkedList() {
    }
    //創建一個鏈表,包含指定傳入的所有元素,這些元素按照迭代順序排列
    public LinkedList(Collection<? extends E> c) {
        this();
        //添加操作
        addAll(c);
    }

其中addAll(c)其實調用了addAll(size,c),由於這里size=0,所以相當於從頭開始一一添加。至於addAll方法,我們暫時不提,當我們總結完普通的添加操作,也就自然明了這個全部添加的操作。

    //把e作為鏈表的第一個元素
    private void linkFirst(E e) {
        //建立臨時節點指向first
        final Node<E> f = first;
        //創建存儲e的新節點,prev指向null,next指向臨時節點
        final Node<E> newNode = new Node<>(null, e, f);
        //這時newNode變成了第一個節點,將first指向它
        first = newNode;
        //對原來的first,也就是現在的臨時節點f進行判斷
        if (f == null)
            //原來的first為null,說明原來沒有節點,現在的newNode
            //是唯一的節點,所以讓last也只想newNode
            last = newNode;
        else
            //原來鏈表不為空,讓原來頭節點的prev指向newNode
            f.prev = newNode;
        //節點數量加一
        size++;
        //對列表進行改動,modCount計數加一
        modCount++;
    }

相應的,把元素作為鏈表的最后一個元素添加和第一個元素添加方法類似,就不贅述了。我們來看看我們一開始遇到的addAll操作,感覺有一點點麻煩的哦:

    //在指定位置把另一個集合中的所有元素按照迭代順序添加進來,如果發生改變,返回true
    public boolean addAll(int index, Collection<? extends E> c) {
        //范圍判斷
        checkPositionIndex(index);
        //將集合轉換為數組,果傳入集合為null,會出現空指針異常
        Object[] a = c.toArray();
        //傳入集合元素個數為0,沒有改變原集合,返回false
        int numNew = a.length;
        if (numNew == 0)
            return false;
        //創建兩個臨時節點,暫時表示新表的頭和尾
        Node<E> pred, succ;
        //相當於從原集合的尾部添加
        if (index == size) {
            //暫時讓succ置空
            succ = null;
            //讓pred指向原集合的最后一個節點
            pred = last;
        } else {
            //如果從中間插入,則讓succ指向指定索引位置上的節點
            succ = node(index);
            //讓succ的prev指向pred
            pred = succ.prev;
        }
        //增強for循環遍歷賦值
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            //創建存儲值尾e的新節點,前向指針指向pred,后向指針指向null
            Node<E> newNode = new Node<>(pred, e, null);
            //表明原鏈表為空,此時讓first指向新節點
            if (pred == null);
                first = newNode;
            else
                //原鏈表不為空,就讓臨時節點pred節點向后移動
                pred.next = newNode;
            //更新新表的頭節點為當前新創建的節點
            pred = newNode;
        }
        //這種情況出現在原鏈表后面插入
        if (succ == null) {
            //此時pred就是最終鏈表的last
            last = pred;
        } else {
            //在index處插入的情況
            //由於succ是node(index)的臨時節點,pred因為遍歷也到了插入鏈表的最后一個節點
            //讓最后位置的pred和succ建立聯系
            pred.next = succ;
            succ.prev = pred;
        }
        //新長度為原長+增長
        size += numNew;
        modCount++;
        return true;
    }
  • 注意:遍歷賦值的過程相當於從pred這個臨時節點開始,依次向后創建新節點,並將pred向后移動,直到新傳入集合的最后一個元素,這時再將pred和succ兩個建立聯系,實現無縫鏈接。

再來看看,在鏈表中普通刪除元素的操作是怎么樣的:

    //取消一個非空節點x的連結,並返回它
    E unlink(Node<E> x) {
        //同樣的,在調用這個方法之前,需要確保x不為空
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        //明確x與上一節點的聯系,更新並刪除無用聯系
        //x為頭節點
        if (prev == null) {
            //讓first指向x.next的臨時節點next,宣布從下一節點開始才是頭
            first = next;
        } else {
            //x不是頭節點的情況
            //讓x.prev的臨時節點prev的next指向x.next的臨時節點
            prev.next = next;
            //刪除x的前向引用,即讓x.prev置空
            x.prev = null;
        }
        //明確x與下一節點的聯系,更新並刪除無用聯系
        //x為尾節點
        if (next == null) {
            //讓last指向x.prev的臨時節點prev,宣布上一節點是最后的尾
            last = prev;
        } else {
            //x不是尾節點的情況
            //讓x.next的臨時節點next的prev指向x.prev的臨時節點
            next.prev = prev;
            //刪除x的后向引用,讓x.next置空
            x.next = null;
        }
        //讓x存儲元素置空,等待GC寵信
        x.item = null;
        size--;
        modCount++;
        return element;
    }

總結來說,刪除操作無非就是,消除該節點與另外兩個節點的聯系,並讓與它相鄰的兩個節點之間建立聯系。如果考慮邊界條件的話,比如為頭節點和尾節點的情況,需要再另加分析。總之,它不需要向ArrayList一樣,拷貝數組,而是改變節點間的地址引用。但是,刪除之前需要找到這個節點,我們還是需要遍歷滴,就像下面這樣:

    //移除第一次出現的元素o,找到並移除返回true,否則false
    public boolean remove(Object o) {
        //傳入元素本身就為null
        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) {
                //刪除的元素不為null,比較值的大小
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

總結一下從前向后遍歷的過程:

  • 創建一個臨時節點指向first。
  • 向后遍歷,讓臨時節點指向它的下一位。
  • 直到臨時節點指向last的下一位(即x==null)為止。

當然特殊情況特殊考慮,上面的remove方法目的是找到對應的元素,只需要在循環中加入相應的邏輯判斷即可。下面這個相當重要的輔助方法就是通過遍歷獲取指定位置上的節點:有了這個方法,我們就可以同過它的前后位置,推導出其他不同的方法:

    //獲得指定位置上的非空節點
    Node<E> node(int index) {
        //在調用這個方法之前會確保0<=inedx<size
        //index和size>>1比較,如果index比size的一半小,從前向后遍歷
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            //退出循環的條件,i==indx,此時x為當前節點
            return x;
        } else {
            //從后向前遍歷
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

與此同時還有indexOflastIndexOf方法也是通過上面總結的遍歷過程,加上計數條件,計算出指定元素第一次或者最后一次出現的索引,這里以indexOf為例:

    //返回元素第一次出現的位置,沒找到就返回-1
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

其實就是我們上面講的遍歷操作嘛,大差不差。有了這個方法,我們還是可以很輕松地推導出另外的contains方法。

    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

然后還是那對基佬方法:getset

    //獲取元素值
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    //用新值替換舊值,返回舊值
    public E set(int index, E element) {
        checkElementIndex(index);
        //獲取節點
        Node<E> x = node(index);
        //存取舊值
        E oldVal = x.item;
        //替換舊值
        x.item = element;
        //返回舊值
        return oldVal;
    }

接下來是我們的clear方法,移除所有的元素,將表置空。雖然寫法有所不同,但是基本思想是不變的:創建節點,並移動,刪除不要的,或者找到需要的,就行了

    public void clear() {
        for (Node<E> x = first; x != null; ) {
            //創建臨時節點指向當前節點的下一位
            Node<E> next = x.next;
            //下面就可以安心地把當前節點有關的全部清除
            x.item = null;
            x.next = null;
            x.prev = null;
            //x向后移動
            x = next;
        }
        //回到最初的起點
        first = last = null;
        size = 0;
        modCount++;
    }

Deque相關操作

我們還知道,LinkedList還繼承了Deque接口,讓我們能夠操作隊列一樣操作它,下面是截取不完全的一些方法:

我們從中挑選幾個分析一下,幾個具有迷惑性方法的差異,比如下面這四個:

public E element() {
    return getFirst();
}
public E getFirst() {
    final Node<E> f = first;
    //如果頭節點為空,拋出異常
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
  • element:調用getFirst方法,如果頭節點為空,拋出異常。
  • getFirst如果頭節點為空,拋出異常
  • peek:頭節點為空,返回null。
  • peekFirst:頭節點為空,返回null。

與之類似的還有:

  • pollFirst和pollLast方法刪除頭和尾節點,如果為空,返回null。
  • removeFirst和removeFirst如果為空,拋異常。

如果有興趣的話,可以研究一下,總之還是相對簡單的。

總結

  • 而LinkedList底層基於雙向鏈表實現,不需要連續的內存存儲,通過節點之間相互引用地址形成聯系。

  • 對於無索引位置的插入來說,例如向后插入,時間復雜度近似為O(1),體現出增刪操作較快。但是如果要在指定的位置上插入,還是需要移動到當前指定索引位置,才可以進行操作,時間復雜度近似為O(n)。

  • Linkedlist不支持快速隨機訪問,查詢較慢

  • 線程不安全,同樣的,關於線程方面,以后學習時再進行總結。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM