LinkedList實現原理

Life journey發表於2020-11-28

LinkedList

LinkedList都是List下面的集合子類,LinkedList中和ArrayList在實現上有很大的區別,ArrayList是維護了一個Object的陣列,而LinkedList在實現上維護了一個頭指標節點物件Node first 和一個尾部指標節點物件Node last,而Node物件是LinkedList的靜態內部類,為什麼要是靜態內部類?因為防止記憶體洩露,因為如果是普通的內部類,那麼就會被其他物件鎖例項化,就會出現記憶體洩露;
在上一篇筆記中,提到了ArrayList和LinkedList的比較,其實在很多場景下,ArrayList用的比較多,而LinkedList使用的場景比較少,因為LinkedList和ArrayList在新增的操作中,資料量沒有達到一定的級別,效能是沒有ArrayList好的,都說LinkedList是使用在寫多讀上的場景下,但是在資料量沒有達一定的情況下,還是ArrayList效能較好;ArrayList 新增大批量資料的時候,耗時主要在資料的動態擴容進行陣列複製的時候比較消耗效能,而LinkedList是每一個新增元素的時候都會建立一個Node節點物件,相當於就是說使用LinkedList的時候,要建立很多Node節點,而這些節點不像普通陣列那樣會存放在一塊連續的記憶體空間中,LinkedList建立的Node物件在記憶體中各個區域中,通過前後指標進行迭代訪問。

LinkedList原始碼分析

LinkedList本身也是List的子類,實現了List介面,具有List中的末尾新增元素、按位置新增元素、刪除元素、迭代元素,而LinkedList還提供了從指定位置進行迭代的功能。
我們都知道ArrayList是通過Object陣列來儲存元素的,而LinkedList是通過Node來儲存元素的,我的理解是這些Node都分配在不連續的空間上面,就是散落在記憶體塊Cell中,當我們要用的時候通過遊標來獲取,我的理解如下:
在這裡插入圖片描述
LinkedList的繼承關係

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

繼承了AbstractSequentialList,本身和AbstractList沒有什麼區別,但是AbstractSequentialList完善了AbstractList中沒有的功能,ArrayList繼承之AbstractList,而AbstractSequentialList是AbstractList的子類,所以AbstractSequentialList就是對AbstractLIst中沒有的功能進行完善。
其中的Cloneable和Serializable和ArrayList一樣,都是標記介面,作用是標記可以複製克隆和序列化。
Deque:實現了Collection中的大家庭介面Queue佇列介面。說明了它具有雙端佇列的功能(LinkedLIst和ArrayList的最大區別就是LinkedList實現了Deque介面,使的LinkedList具有了雙端佇列的功能)

基本屬性

我這邊提一下transient的作用,簡單點說就是這個關鍵字就是序列化的作用域,加了這個關鍵字,那麼被修飾的屬性就不會被序列化了。為什麼呢?因為我們的size是根據我們的集合大小而動態變化的,我們如果序列化出來過後,或者clone出來過後都是初始為0,我們不可能我們對集合序列化過後得到的是0,而且這樣就節省空間,所以加了這個關鍵字就不會被序列化出來了。


//當前集合有多少個元素,泛指集合大小
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;

//靜態內部類,為了防止記憶體洩漏,設定為靜態內部類
private static class Node<E> {
    E item;//節點中的具體元素資料
    Node<E> next;//後繼指標遊標
    Node<E> prev;//前驅指標遊標
   //一般通過Node構造來建立Node節點物件
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

構造方法

//這個構造方法是預設將一個集合轉換成一個LinkedList集合,
//完成過後,原有的集合物件就轉成了LinkedList物件了
public LinkedList(Collection<? extends E> c) {
    this();//呼叫預設的構造,現在預設的構造是一個空殼方法
    //將集合全部新增到LinkedList中
    addAll(c);
}

addAll方法


public boolean addAll(Collection<? extends E> c) {
    //這裡會把當前的size傳入,如果是通過構造方法進入的,
    //那麼這個size是為0、
    //如果是先建立了一個LinkedList過後,並且已經新增了元素過後,
    //再來呼叫這個addall,
    //那麼這個size就不是空的,這個size就等於LinkedLIst的集合大小
    return addAll(size, c);
}
//真正的新增集合到LinkedList的核心方法
public boolean addAll(int index, Collection<? extends E> c) {
    //這裡檢查下標是否越界的方法
    //判斷的核心就是當前index是否是大於等於0並且小於等於size的
    //因為這個方法入口不僅僅是通過構造呼叫的,還可以通過外部的
    //LinkedList物件進行呼叫,因為這個方法是public
    checkPositionIndex(index);
    //先將要新增到LinkedList的集合collection轉成一個普通的Object陣列
    Object[] a = c.toArray();
    //得到轉換過後的陣列長度
    int numNew = a.length;
    //如果陣列是等於0的,那麼要新增的集合是空的,就不用執行後面的方法
    if (numNew == 0)
        return false;

    //什麼Node節點的前驅pre和後繼next
    Node<E> pred, succ;
    //如果index == size,那麼就有兩種可能
    //1.通過構造方法進行呼叫,這個時候index和size都是0
    //2.通過外部物件呼叫時,從尾部進行新增,也就是傳入的index
    //是通過LinkedList的物件.size()獲取到的,就是新增到尾部
    if (index == size) {
        //這裡如果是通過構造進來的,那麼succ=last=first=null
        //如果是通過外部呼叫的,那麼就是last=pred,也就是說
        //從尾部開始新增,尾部的最後一個元素為前驅
        succ = null;
        pred = last;
    } else {
        //否則的話,從集合的某一個位置進行新增的,先獲取到這個位置的
        //遊標指標,找到過後把遊標指標的前驅賦值給pred
        succ = node(index);
        pred = succ.prev;
    }

    for (Object o : a) {
        //e=Node.item
        @SuppressWarnings("unchecked") E e = (E) o;
        //建立一個Node物件,設定前驅,元素,後驅=null
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
         //如果pred為空,則證明是通過構造呼叫的,last=first=null
         //新建立Node賦值給first,這裡也只有第一次能進入,第二次
         //迴圈就不會進到這裡了
            first = newNode;
            
        else
        //當前元素指向pred的後繼
        pred.next = newNode;
        
        
        //將當前建立的node賦值給pred,作為下一個Node前驅
        //這裡很重要,這塊要自己去意會設計者的思路
        //比如方法是通過構造進來的:那麼pred肯定是空的,
        //所以第一個節點first也就是第一次迴圈建立的node,
        //把當前建立的node作為Node的第一個元素node,而第二次迴圈開始
        //每個節點的next為新建立的node,而上一個元素node作為下一個、
        //元素pred,這塊只有去意會,好好理解一番,從你是設計者的角度
        //去思考為什麼要這麼做
        pred = newNode;
    }

    if (succ == null) {
        //如果succ為空,則有兩種情況:、
        //1.從構造呼叫進來,那麼first=last=null,
        //當執行完成過後,last就是pred
        //第二種情況是從尾部新增的
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

   //元素個數計算
    size += numNew;
    //運算元+1,記錄集合的運算元
    modCount++;
    return true;
}

Node<E> node(int index) {
    // assert isElementIndex(index);
   //  if (index < (size >> 1)) 這行程式碼就是程式碼設計者的高明之處
   //說白了就是通過看你的index到底是在集合的前半部分還是後半部分
   //簡單來說設計者是這樣考慮的:比如集合是20,你傳入的index是3
   //那麼我就從開頭開始尋找,如果你傳入的是15,那麼我就從後面開始尋找
   //就是定址快,能快速找到我們的Node節點
    if (index < (size >> 1)) {//二分查詢方法
    //如果index小於size的一半,那麼從first開始找
        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;
    }
}

Add

//新增元素,從尾部新增
public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    //新增元素,判斷是否是第一個元素,如果是LinkedList
    //建立過後的第一次新增,這first就是當前建立的節點node
    //否則設定下一個節點也就是last.next就是當前建立的節點物件newNode
    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++;
}

Add指定位置新增

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

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
//指位新增方法核心邏輯  操作新節點,
//緊接修改原有節點的前驅屬性,最後再修改前驅節點的後繼屬性
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;//原位置節點的前驅pred
    final Node<E> newNode = new Node<>(pred, e, succ);//建立新節點,設定新節點其前驅為原位置節點的前驅pred,其後繼為原位置節點succ
    succ.prev = newNode;//將新節點設定到原位置節點的前驅
    if (pred == null)//前驅如果為空,空連結串列,則新節點設定為first
        first = newNode;
    else
        pred.next = newNode;//將新節點設定到前驅節點的後繼
    size++;//修改當前list的size
    modCount++;//記錄該list物件被執行修改的次數。
}

final修飾,不希望在執行時對變數做重新賦值
LinkedList 在插入資料優於ArrayList ,主要是因為他只需要修改指標的指向即可,而不需要將整個陣列的資料進行轉移。而LinkedList 由於沒有實現 RandomAccess,或者說不支援索引搜尋的原因,他在查詢元素這一操作,需要消耗比較多的時間進行操作(n/2)。

GET

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

從上面的獲取元素get方法就可以看出來為什麼LinkedList的讀取效率不如ArrayList了,每一次LinkedList都要通過index去尋找Node,雖然已經做了優化處理,比如是從前還是從後去找,但是肯定是沒有ArrayList直接通過下標獲取元素來的快,所以|ArrayList實現了RandomAccess隨機訪問標記介面,而LinkedList是沒有的。

SET

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

在指定位置替換原有的元素,太簡單了,不做介紹了,就是原來位置=3的地方放入的是“abc”,而通過set(3,“ert”)成功過後,位置還是3,值變成了ert.

通過指定位置迭代

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

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

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }

    public boolean hasPrevious() {
        return nextIndex > 0;
    }

    public E previous() {
        checkForComodification();
        if (!hasPrevious())
            throw new NoSuchElementException();

        lastReturned = next = (next == null) ? last : next.prev;
        nextIndex--;
        return lastReturned.item;
    }

    public int nextIndex() {
        return nextIndex;
    }

    public int previousIndex() {
        return nextIndex - 1;
    }

    public void remove() {
        checkForComodification();
        if (lastReturned == null)
            throw new IllegalStateException();

        Node<E> lastNext = lastReturned.next;
        unlink(lastReturned);
        if (next == lastReturned)
            next = lastNext;
        else
            nextIndex--;
        lastReturned = null;
        expectedModCount++;
    }

    public void set(E e) {
        if (lastReturned == null)
            throw new IllegalStateException();
        checkForComodification();
        lastReturned.item = e;
    }

    public void add(E e) {
        checkForComodification();
        lastReturned = null;
        if (next == null)
            linkLast(e);
        else
            linkBefore(e, next);
        nextIndex++;
        expectedModCount++;
    }

    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (modCount == expectedModCount && nextIndex < size) {
            action.accept(next.item);
            lastReturned = next;
            next = next.next;
            nextIndex++;
        }
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

迭代都差不多,都有一個共同特徵就是檢查異常,fast-fail快速失敗
linkedlist提供了向前迭代和向後迭代,這是和ArrayList的主要區別之一。

刪除元素

1.AbstractSequentialList的remove

public E remove(int index) {
    checkElementIndex(index);
    //node(index)找到index位置的元素
    return unlink(node(index));
}
//remove(Object o)這個刪除元素的方法的形參o是資料本身,而不是LinkedList集合中的元素(節點),所以需要先通過節點遍歷的方式,找到o資料對應的元素,然後再呼叫unlink(Node x)方法將其刪除
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;
}
E unlink(Node<E> x) {
    //x的資料域element
    final E element = x.item;
    //x的下一個結點
    final Node<E> next = x.next;
    //x的上一個結點
    final Node<E> prev = x.prev;
    //如果x的上一個結點是空結點的話,那麼說明x是頭結點
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;//將x的前後節點相連   雙向連結串列
        x.prev = null;//x的屬性置空
    }
    //如果x的下一個結點是空結點的話,那麼說明x是尾結點
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;//將x的前後節點相連   雙向連結串列
        x.next = null;
    }
    x.item = null;//指向null  方便GC回收
    size--;
    modCount++;
    return element;
}

2.Deque 中的Remove

//將first 節點的next 設定為新的頭節點,然後將 f 清空。 removeLast 操作也類似。
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    //獲取到頭結點的下一個結點           
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // 方便 GC
    //頭指標指向的是頭結點的下一個結點
    first = next;
    //如果next為空,說明這個連結串列只有一個結點
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

雙端連結串列(佇列Queue)

java中佇列的實現就是LinkedList: 我們之所以說LinkedList 為雙端連結串列,是因為他實現了Deque 介面;我們知道,佇列是先進先出的,新增元素只能從隊尾新增,刪除元素只能從隊頭刪除,Queue中的方法就體現了這種特性。 支援佇列的一些操作,我們來看一下有哪些方法實現:
pop()是棧結構的實現類的方法,返回的是棧頂元素,並且將棧頂元素刪除
poll()是佇列的資料結構,獲取對頭元素並且刪除隊頭元素
push()是棧結構的實現類的方法,把元素壓入到棧中
peek()獲取隊頭元素 ,但是不刪除佇列的頭元素
offer()新增隊尾元素
可以看到Deque 中提供的方法主要有上述的幾個方法,接下來我們來看看在LinkedList 中是如何實現這些方法的。
1.佇列的增
offer()新增隊尾元素

public boolean offer(E e) {
    return add(e);
}

具體的實現就是在尾部新增一個元素
2、佇列的刪
poll()是佇列的資料結構,獲取對頭元素並且刪除隊頭元素

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

具體的實現前面已經講過,刪除的是佇列頭部的元素
3.佇列的查
peek()獲取隊頭元素 ,但是不刪除佇列的頭元素

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

4.棧的增
push()是棧結構的實現類的方法,把元素壓入到棧中
push() 方法的底層實現,其實就是呼叫了 addFirst(Object o)

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

5.棧的刪
pop()是棧結構的實現類的方法,返回的是棧頂元素,並且將棧頂元素刪除

public E pop() {
    return removeFirst();
}
public E removeFirst() {
    final Node f = first;
    if (f == null)
    throw new NoSuchElementException();
    return unlinkFirst(f);
}

相關文章