LinkedList原始碼解析(jdk1.8)

俺就不起網名發表於2018-04-17

本篇介紹的LinkedList是List介面的另一種實現,它的底層是基於雙向連結串列實現的,它具有插入刪除快而查詢修改慢的特點,此外,通過對雙向連結串列的操作還可以實現佇列和棧的功能。

一、類的繼承

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

二、類的屬性

//集合size
transient int size = 0;
//頭結點
transient Node<E> first;
//尾結點
transient Node<E> last;

LinkedList的底層結構如下圖所示:


F表示頭結點引用,L表示尾結點引用,連結串列的每個結點都有三個元素,分別是前繼結點引用(P),結點元素的值(E),後繼結點的引用(N)。結點由內部類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;
        }
    }

Node這個內部類只有三個成員變數和一個構造器,item表示結點的值,next為下一個結點的引用,prev為上一個結點的引用,通過構造器傳入這三個值。

三、建構函式

    public LinkedList() {
    }
    public LinkedList(Collection<? extends E> c) {
        this(); //先呼叫無參構造期
        addAll(c);//呼叫add函式
    }

有兩個構造器,一個是無參構造器,一個是傳入外部集合的構造器。與ArrayList不同的是LinkedList沒有指定初始大小的構造器。

四、add函式

1、在瞭解add方法之前,我們先了解LinkedList中linkLast(E)/linkBefore(E, Node<E>) ,這兩個方法是增加節點的方法:

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

b、linkBefore方法:獲取引數節點的前一個節點,如果為空則生成的新節點為第一個節點,否則生成的新指定節點就是傳入引數節點的前一個節點

    void linkBefore(E e, Node<E> succ) {
        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++;
    }

2、下面是關於add函式的幾個方法:

	//方法1:在當前集合最後面加一個節點
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    //方法2:根據下標在指定位置插入函式
    public void add(int index, E element) {
        checkPositionIndex(index); //檢查下標是否越界
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index)); //node(index):返回指定位置的node,詳細見下
    }

    //方法3:當前集合新增集合結果
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c); //將集合改成陣列,然後迴圈利用node的屬性新增集合元素(節點)
    }

add(int index, E element)呼叫了node(index),程式碼如下:

   Node<E> node(int 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;
        }
    } 
總結:
1、node(index)其實就是根據下標,然後迴圈依次獲取下個節點(或上個節點,二分法),獲得最紅節點;

2、add函式其實就是插入的時候,如果有指定的下標位置,那麼將後面的節點往後移一位,如果沒有指定下標位置就直接在連結串列最後新增一個節點。

五、remove函式

	//根據下標刪除
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    //刪除指定元素
    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函式找到刪除元素之後,呼叫了方法 unlink(Node) 進行節點刪除,它的程式碼如下:

E unlink(Node<E> x) {
        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節點為給定節點的前一個節點(倒數第二個)
            last = prev;
        } else { //將下一個結點的前繼結點引用指向給定結點的前繼結點
            next.prev = prev;
            x.next = null; //將給定結點的下一個結點置空
        }

        x.item = null; //將節點元素設定為空
        size--;
        modCount++;
        return element;
    }

總結:remove函式第一步找到要刪除的元素,第二步則處理集合元素的要刪除節點的上個節點和下個節點;

六、get/set函式

	    //查詢,根據下標獲取,呼叫node(index)
    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;
    }

總結:對連結串列的查詢和修改操作都需要遍歷連結串列進行元素的定位,所以效率會低一點;

七、實現佇列和棧的操作
佇列:是一種特殊的線性表,特殊之處在於它只允許在表的前端進行刪除操作,而在表的後端進行插入操作。(先進先出)
棧:又名堆疊,它是一種運算受限的線性表,其限制是僅允許在表的一端進行插入和刪除運算。(先進後出)
LinkedList資料結構是基於連結串列的,所以通過對雙向連結串列的操作還可以實現單項佇列,雙向佇列和棧的功能。

不管是單向佇列還是雙向佇列還是棧,其實都是對連結串列的頭結點和尾結點進行操作,它們的實現都是基於addFirst(),addLast(),removeFirst(),removeLast()這四個方法,它們的操作和linkBefore()和unlink()類似,只不過一個是對連結串列兩端操作,一個是對連結串列中間操作。可以說這四個方法都是linkBefore()和unlink()方法的特殊情況。

八、總結
1、LinkedList是基於雙向連結串列實現的,不論是增刪改查方法還是佇列和棧的實現,都可通過操作結點實現
2、LinkedList無需提前指定容量,因為基於連結串列操作,集合的容量隨著元素的加入自動增加
3、LinkedList刪除元素後集合佔用的記憶體自動縮小,無需像ArrayList一樣呼叫trimToSize()方法
4、LinkedList的所有方法沒有進行同步,因此它也不是執行緒安全的,應該避免在多執行緒環境下使用

參考部落格:點選開啟連結



相關文章