jdk原始碼分析之LinkedList

王世暉發表於2016-05-23

LinkedList關鍵屬性

size表示當前連結串列儲存了多少資料,first指標指向連結串列第一個資料,last指標指向連結串列最後一個資料

    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底層資料結構

Linked是一個雙向連結串列,連結串列節點的資料如下,有一個資料域和兩個指標域

    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首尾新增資料linkFirst(E e)和linkLast(E e)

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

先儲存舊連結串列的尾指標為l,然後根據傳入的資料e和為指標l構建一個資料節點。

newNode = new Node<>(l, e, null)

將連結串列的尾節點修改為新建立的節點newNode。如果l==null,表示之前連結串列裡邊沒有儲存資料,現在新增了一個新資料,頭指標和尾指標都應該指向這個新新增的節點

        last = newNode;
        if (l == null)
            first = newNode;

如果之前連結串列裡邊儲存的有資料,l!=null,則修改新節點連結到原連結串列的尾部

l.next = newNode;

然後修改size和modCount(迭代器併發訪問用到的屬性)
linkFirst(E e)程式碼類似,原理一樣

在LinkedList的某一節點前插入資料linkBefore

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

在節點succ前插入資料 e,則succ是e的後繼節點,e的前序就是succ的前序,因此根據e、前序、後繼構造一個新的節點newNode = new Node<>(pred, e, succ),修改succ的前序節點為新節點newNode,succ.prev = newNode,然後再將斷開的連結串列連結起來

LinkedList刪除頭結點unlinkFirst和尾節點unlinkLast

    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

首先儲存頭結點的後繼節點指標(即將成為新的頭結點),然後頭結點指標域和資料域置null,加快記憶體回收速度,然後判斷next是否為null,null的話表示原連結串列只有一個節點,現在還把唯一的節點刪除了,因此first頭指標和last尾指標都應該為null。next不為null的話表示刪除頭結點後連結串列還有資料,修改next的prev指標為null即可(pre指標為null表示此接節點為隊首資料,next指標為null表示隊尾資料)
unlinkLast(Node l) 程式碼和原理類似

LinkedList刪除某一節點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;
    }

仍然是指標操作,提取待刪除節點的資料域、前序和後繼,資料域用於返回值,前序和後繼用於連結串列的斷連操作
前序為null表示帶刪除節點是隊首節點,修改first指標為待刪除節點的後續節點
前序節點不為null就把前序節點的next指標指向後繼節點,待刪除節點的prev指標域置null,這樣待刪除節點的前序節點指標已經斷開重連了,然後處理後繼節點指標的斷開重連
判斷後繼節點是否為null,是null的話表示待刪除節點就是隊尾,修改隊尾指標last為待刪除節點的前序
後繼節點不為null,後繼節點的prev指標指向待刪除節點的前序節點,待刪除節點的next指標置null,這樣待刪除節點的後繼結點的指標也已經斷開重連了
然後把待刪除節點的資料域置null,修改size和modCount,返回刪除節點的資料

LinkedList的隨機訪問node(int index)

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

連結串列插入刪除新增資料比較方便,直接修改指標的斷連即可。但是連結串列並不能夠像陣列那樣隨機訪問,只能進行遍歷。LinkedList是一個雙向連結串列,可以對遍歷做一個小優化,只遍歷半個連結串列即可。
如果位置在連結串列前半部分index < (size >> 1),正向遍歷查詢for (int i = 0; i < index; i++)
否則逆向遍歷查詢for (int i = size - 1; i > index; i–)

LinkedList的某一位置新增一個集合addAll

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

首先檢查插入位置是否合法

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

可見插入位置區間為【0,size】,可以隊首插入,可以隊尾插入,中間任意位置當然也可以
然後把集合物件轉化為陣列,陣列長度為0表示集合沒有資料,直接返回false表示插入失敗
構建插入節點的前序和後繼,如果插入位置index==size的話表示隊尾插入資料,因此後續為null,前序為隊尾last

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } 

不是在隊尾插入集合的話需要根據插入位置index找到後繼節點,找到了後繼也就找到了前序

        else {
            succ = node(index);
            pred = succ.prev;
        }

Node node(int index)方法用於返回特定位置的節點,這樣插入點的前序和後繼都找到了,開始迴圈把集合的資料新增到連結串列的特定位置

        for (Object o : a) {
            E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

通過new Node<>(pred, e, null)建立新節點,前序指標在建立的時候在建構函式裡邊已經指明。
pred == null表示是在隊首新增資料,需要修改隊首指標first
否則把前序節點的後繼指標設定為newNode
再把前序節點設為newNode,接著下一次迴圈
這樣在迴圈結束後,只有最後一個新增的節點的後繼指標沒有處理

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

如果succ後繼為null,修改隊尾指標為last為最後一個新增的節點pred
否則設定最有一個新增的節點的後繼指標為succ,succ的前序指標指向最後一個新增的節點pred
最後修改size和modCount,返回true表示新增成功

LinkedList的雙端佇列特性

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

LinkedList實現了Deque介面,因此可以吧LinkedList當做一個雙端佇列屬性,比如peek和poll,均是通過連結串列的基本操作實現相應的功能,其他的方法類似,不再一一列舉。

    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

相關文章