Java 源代碼解析 | 集合類 | LinkedList

LinkedList裏面涉及到的一些操做,很是細緻,以免出現的空指針,理解後對於其優勢與缺點會有一個更加總體的認識吧。

繼承關係圖(對比ArrayList)

簡單繼承關係圖

元素的存儲結構

LinkedList中,每個元素都是Node存儲,Node擁有一個存儲值的item與一個前驅prev和一個後繼next,以下:java

// 典型的鏈表結構
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;
    }
}

構造函數與成員變量

變量主要有3個:node

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中的構造函數有兩個,一個是無參的,另外一個是帶Collection參數的。數組

public LinkedList() {}//無參構造函數
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);//將c中的元素都添加到此列表中
}

其添加的過程當中,此時size = 0,以下:函數

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);//此時 size == 0
}

若是index==size,則添加c中的元素到列表的尾部;不然,添加的第index個元素的前面;測試

public boolean addAll(int index, Collection<? extends E> c) {
    // 檢查位置是否合法 位置是[0,size],注意是閉區間 不然報異常
    checkPositionIndex(index);
    Object[] a = c.toArray();// 獲得一個元素數組
    int numNew = a.length;// c中元素的數量
    if (numNew == 0)
        return false;// 沒有元素,添加失敗
        
    // 主要功能是找到第size個元素的前驅和後繼。獲得此元素須要分狀況討論。
    // 這段代碼是各類狀況的總和,可能有一點點容易懵逼。
    Node<E> pred, succ;// 前驅與後繼
    if (index == size) {// 若是位置與當前的size相同
        succ = null;// 無後繼
        pred = last;// 前驅爲last,即第size個元素(最後一個元素)
    } else {// 若與size不一樣,即index位於[0, size)之間
        succ = node(index);// 後繼爲第index個元素
        pred = succ.prev;// 前驅爲後繼的前驅
    }// 後文有詳細的圖片說明
    // 開始逐個插入
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        // 新建一個以pred爲前驅、null爲後繼、值爲e的節點
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)// 前驅爲空,則此節點被當作列表的第一個節點
            first = newNode;
        else// 規避掉了NullPointerException,感受又達到了目的,又實現了邏輯
            pred.next = newNode;// 不爲空,則將前驅的後繼改爲當前節點
        pred = newNode;// 將前驅改爲當前節點,以便後續添加c中其它的元素
    }
    // 至此,c中元素已添加到鏈表上,但鏈表中從size開始的那些元素尚未連接到列表上
    // 此時就須要利用到以前找出來的succ值,它是做爲這個c的總體後繼
    if (succ == null) {// 若是後繼爲空,說明無總體後繼
        last = pred;// c的最後一個元素應看成爲列表的尾元素
    } else {// 有總體後繼
        pred.next = succ;// pred即c中的最後一個元素,其後繼指向succ,即總體後繼
        succ.prev = pred;// succ的前驅指向c中的最後一個元素
    }
    // 添加完畢,修改參數
    size += numNew;
    modCount++;
    return true;
}

返回序號爲index的元素節點。看這段代碼中的if語句,真的是佩服,這樣寫代碼,均可以這樣減小查找次數。this

Node<E> node(int index) {
    // assert isElementIndex(index);
    // 這個地方頗有意思。視其與中值的差距,以爲從前遍歷仍是從後遍歷。
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 循環index次 迭代到所須要的元素
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        // 循環size-1-index次
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

測試代碼以及驗證輸出以下:spa

public class Main {
    public static void main(String[] args) {
        List<String> list = new LinkedList<>(Arrays.asList("1", "2", "3"));
        System.out.println(list.toString());
        list.addAll(2, Arrays.asList("4", "5"));
        System.out.println(list.toString());
        list.addAll(0, Arrays.asList("6", "7"));
        System.out.println(list.toString());
    }
}
---
[1, 2, 3]
[1, 2, 4, 5, 3]
[6, 7, 1, 2, 4, 5, 3]

增長元素

對於向列表中添加元素,先看一組基本的添加操做,具體以下:3d

將e連接成列表的第一個元素

源代碼以及相應的分析以下:指針

private void linkFirst(E e) {
    final Node<E> f = first;
    // 前驅爲空,值爲e,後繼爲f
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;// first指向newNode
    // 此時的f有可能爲null
    if (f == null)// 若f爲空,則代表列表中尚未元素
        last = newNode;// last也應該指向newNode
    else
        f.prev = newNode;// 不然,前first的前驅指向newNode
    size++;
    modCount++;
}

其過程大體以下兩圖所示:
初始狀態:
這裏寫圖片描述
後續狀態:
添加元素做爲第一個元素時,所須要作的工做,有下列所述:
首先,獲取第一個節點,而後將該節點的前驅指向新添加的元素所在的節點;
接着,將新添加的節點的後繼指向前第一個節點;
最後,將first指向新添加的元素的節點。添加完畢。
這裏寫圖片描述code

將e連接爲最後一個元素

源代碼以及相應的解釋以下:

void linkLast(E e) {
    final Node<E> l = last;// 找到最後一個節點
    // 前驅爲前last,值爲e,後繼爲null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;// last必定會指向此節點
    if (l == null)// 最後一個節點爲空,說明列表中無元素
        first = newNode;// first一樣指向此節點
    else
        l.next = newNode;// 不然,前last的後繼指向當前節點
    size++;
    modCount++;
}

其操做過程與前述linkFirst()的過程相似,所以其替換後的示意圖以下:
這裏寫圖片描述

將e連接到節點succ前

源代碼以及相應的解析以下:

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev; // 找到succ的前驅
    // 前驅爲pred,值爲e,後繼爲succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 將succ的前驅指向當前節點
    succ.prev = newNode;
    if (pred == null)// pred爲空,說明此時succ爲首節點
        first = newNode;// 指向當前節點
    else
        pred.next = newNode;// 不然,將succ以前的前驅的後繼指向當前節點
    size++;
    modCount++;
}

這個操做有點相似將上述的兩個操做整合到一塊兒。其操做簡圖以下:
這裏寫圖片描述


有了上述的分析,咱們再來看一些添加的操做,這些操做基本上是作了一些邏輯判斷,而後再調用上述三個方法去實現添加功能,這裏略過就好。

public boolean add(E e) {
     linkLast(e);
     return true;
 }
 // 只有這個是有一點邏輯的
 public void add(int index, E element) {
     checkPositionIndex(index);
     if (index == size)// 爲最後一個節點,固然是添加到最後一個~
         linkLast(element);
     else
         linkBefore(element, node(index));
 }
 public void addFirst(E e) {
     linkFirst(e);
 }
 public void addLast(E e) {
     linkLast(e);
 }

刪除元素

刪除就是添加過程的逆過程。一樣,在分析咱們使用的接口前,先分析幾個咱們看不到的方法,以下:

刪除首節點

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;// first指向前first的後繼,也就是列表中的2號位
    if (next == null)// 若是此時2號位爲空,那麼列表中此時已無節點
        last = null;// last指向null
    else
        next.prev = null;// 首節點無前驅
    size--;
    modCount++;
    return element;// 返回首節點保存的元素值
}

刪除尾節點

此處的操做與刪除首節點的操做相似。

private E unlinkLast(Node<E> l) {
// assert l == last && l != null;別忽略這裏的斷言
final E element = l.item;// 取出尾節點中的元素
final Node<E> prev = l.prev;// 取出尾節點中的後繼
l.item = null;
l.prev = null; // help GC
last = prev;// last指向前last的前驅,也就是列表中的倒數2號位
if (prev == null)// 若是此時倒數2號位爲空,那麼列表中已無節點
    first = null;// first指向null
else
    prev.next = null;// 尾節點無後繼
size--;
modCount++;
return element;// 返回尾節點保存的元素值
}

刪除某個非空節點

這個也相似添加元素時的第三個基本操做,與結合了上述兩個操做有點相似。

// x即爲要刪除的節點
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;// 保存x的元素值
final Node<E> next = x.next;// 保存x的後繼
final Node<E> prev = x.prev;// 保存x的前驅

if (prev == null) {// 前驅爲null,說明x爲首節點
    first = next;// first指向x的後繼
} else {
    prev.next = next;// x的前驅的後繼指向x的後繼,即略過了x
    x.prev = null;// x.prev已無用處,置空引用
}

if (next == null) {// 後繼爲null,說明x爲尾節點
    last = prev;// last指向x的前驅
} else {
    next.prev = prev;// x的後繼的前驅指向x的前驅,即略過了x
    x.next = null;// x.next已無用處,置空引用
}

x.item = null;// 引用置空
size--;
modCount++;
return element;// 返回所刪除的節點的元素值
}

有了上面的幾個函數做爲支撐,咱們再來看下面的幾個咱們能用來刪除節點的方法,他們也基本上是在一些邏輯判斷的基礎之上,再調用上述的基本操做:

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// 遍歷列表中全部的節點,找到相同的元素,而後刪除它
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;
}
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

修改元素

經過遍歷,循環index次,獲取到相應的節點後,再經過節點來修改元素值。

public E set(int index, E element) {
     checkElementIndex(index);
     Node<E> x = node(index);// 獲取到須要修改元素的節點
     E oldVal = x.item;// 保存以前的值
     x.item = element;// 修改
     return oldVal;// 返回修改前的值
 }

查詢元素

經過位置,循環index次,獲取到節點,而後返回該節點中元素的值

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;// 獲取節點,並返回節點中的元素值
}

還有兩個獲取首尾節點的元素的方法:

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

獲取元素位置

從0開始日後遍歷

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {// 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;// 未找到,返回-1
}

size - 1開始遍歷。基本操做與上述操做相似,只是起始位置不一樣。

public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    return -1;
}

額外的話

在上面的諸多函數中,有許可能是須要進行位置判斷的。在源碼中,位置判斷有兩個函數,一個是下標,一個是位置。看到這兩個函數,確實是有一些感觸,這確實是須要比較強的總結能力以及仔細的觀察能力。

// 下標,保證數組訪問不越界。
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}
// 位置
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

後記

LinkedList還實現了Queue這個接口,在實現這些接口時,仍然是作一些邏輯處理,而後調用上面所描述的基本操做,如link()unlink()之類的,所以再也不分析。還有其中的關於序列化、Iterator這兩塊,與ArrayList的實現也是不盡相同的,故在此可參考ArrayList中的解析。

相關文章
相關標籤/搜索