1. 程式人生 > >Java集合——LinkedList原始碼分析

Java集合——LinkedList原始碼分析

一 前言

上一篇我們介紹了ArrayList原始碼解析有想看的同學可以點選這個連結ArrayList原始碼解析。平時我們或多或少都用過LinKedList,但是對其原理不是很瞭解,我們就來一起學習吧。

二 原始碼解析

1. LinkedList概述

LinkedList是一個實現了List介面和Deque介面的雙端連結串列。
有關索引的操作可能從連結串列頭開始遍歷到連結串列尾部,也可能從尾部遍歷到連結串列頭部,這取決於看索引更靠近哪一端。
LinkedList不是執行緒安全的,如果想使LinkedList變成執行緒安全的,可以使用如下方式:

List list=Collections.synchronizedList(new LinkedList(...
));
  1. LinkedList底層是雙向連結串列儲存資料,並且記錄了頭節點和尾節點
  2. 插入和刪除比較快(O(1)),查詢則相對慢一些(O(n))。
  3. 刪除也是非常快,只需要改動一下指標就行了,代價很小.
  4. 新增元素非常快,如果是新增到頭部和尾部的話更快,因為已經記錄了頭節點和尾節點,只需要連結一下就行了. 如果是新增到連結串列的中間部分的話,那麼多一步操作,需要先找到新增索引處的元素(因為需要連結到這裡),才能進行新增.
  5. 因為是連結串列結構,所以分配的空間不要求是連續的
  6. 執行緒不安全
  7. iterator()和listIterator()返回的迭代器都遵循fail-fast機制。
  8. 遍歷的時候,建議採用forEach()進行遍歷,這樣可以在每次獲取下一個元素時都非常輕鬆(next = next.next;). 然後如果是通過fori和get(i)的方式進行遍歷的話,效率是極低的,每次get(i)都需要從最前面(或者最後面)開始往後查詢i索引處的元素,效率很低.

LinkedList底層是連結串列結構,說具體點他是雙向迴圈連結串列。什麼是雙向連結串列呢?
這裡寫圖片描述

雙向連結串列的每個節點包含以下資料:上一個節點的指標,自己的資料,下一個節點的指標.尾節點沒有下一個節點,所以指向null.這樣的結構,比如我拿到連結串列中間的一個節點,即可以往前遍歷,也可以往後遍歷.

2 LinkedList繼承關係

先上一盤菜

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

這裡寫圖片描述

這裡寫圖片描述

  1. LinkedList繼承於AbstractSequentialList,AbstractSequentialList這個類提供了List的一個骨架實現介面,以儘量減少實現此介面所需的工作量由“順序訪問”資料儲存(如連結列表)支援。對於隨機訪問資料(如陣列),應使用AbstractList優先於此類。
  2. LinkedList 實現了List介面,意味著LinkedList元素是有序的,可以重複的,可以有null元素的集合。
  3. LinkedList 實現 Deque 介面,Deque是Queue的子介面,Queue是一種佇列形式,而Deque是雙向佇列,它支援從兩個端點方向檢索和插入元素。
  4. LinkedList 實現了Cloneable介面,即覆蓋了函式clone(),能克隆,可以被複制.注意,LinkedList裡面的clone()複製其實是淺複製。
  5. LinkedList 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸。

3 LinkerList 全域性變數

LinkedList本身的的屬性比較少,主要有三個:

  1. size 當前有多少個節點。
  2. first 代表第一個節點。
  3. last 代表最後一個節點。
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{   
    //當前有多少個節點
    transient int size = 0;
    //第一個節點
    transient Node<E> first;
    //最後一個節點
    transient Node<E> last;
}

這個 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是連結串列的節點,Node是LinkedList的靜態內部類,資料結構也比較簡單,如下:
1. item 該節點的資料。
2. next 指向下一個節點的指標。
3. prev 指向上一個節點的指標。

4 構造方法

/**
* 構造一個空列表
*/
public LinkedList() {
}

/**
* 構造列表通過指定的集合
*/
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList一共有2個構造方法,第一個構造一個空列表,第二個構造列表通過指定的集合。我們主要看第二個,

//將指定集合的所有元素插入到末尾位置
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

//將指定集合的所有元素插入到index位置
public boolean addAll(int index, Collection<? extends E> c) {
    //1. 入參合法性檢查
    checkPositionIndex(index);

    //2. 將集合轉成陣列
    Object[] a = c.toArray();
    //3. 記錄需要插入的集合元素個數
    int numNew = a.length;
    //4. 如果個數為0,那麼插入失敗,不繼續執行了
    if (numNew == 0)
        return false;

    //5. 判斷一下index與size是否相等
    //相等則插入到連結串列末尾
    //不相等則插入到連結串列中間  index處   
    Node<E> pred, succ;   
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        //找到index索引處節點  這樣就可以方便的拿到該節點的前後節點資訊
        succ = node(index);
        //記錄index索引處節點前一個節點
        pred = succ.prev;
    }

    //6. 迴圈將集合中所有元素連線到pred後面
    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;
    }

    //7. 判斷succ是否為空
    //為空的話,那麼集合的最後一個元素就是尾節點
    //非空的話,那麼將succ連線到集合的最後一個元素後面
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    //8. 連結串列長度+numNew
    size += numNew;
    modCount++;
    return true;
}

解析:addAll 這段程式碼分為了2種情況,一個是原來的連結串列是空的,一個是原來的連結串列有值。我們這邊是構造方法是第一種情況。

  1. 將需要新增的集合轉成陣列a。
  2. 判斷需要插入的位置index是否等於連結串列長度size,如果相等則插入到連結串列最後;如果不相等,則插入到連結串列中間,還需要找到index處節點succ,方便拿到該節點的前後節點資訊.
  3. 記錄index索引處節點的前一個節點pred,迴圈將集合中所有元素連線到pred的後面
  4. 將集合最後一個元素的next指標指向succ,將succ的prev指標指向集合的最後一個元素

5 新增元素各種方法

5.1 add(E e)
/**
* 新增指定元素到連結串列尾部
*/
public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
* Links e as last element.將e新增到尾部
*/
void linkLast(E e) {
    //1. 暫記尾節點
    final Node<E> l = last;
    //2. 構建節點 前一個節點是之前的尾節點
    final Node<E> newNode = new Node<>(l, e, null);
    //3. 新建的節點是尾節點了
    last = newNode;
    //4. 判斷之前連結串列是否為空  
    //為空則將新節點賦給頭結點(相當於空連結串列插入第一個元素,頭結點等於尾節點)
    //非空則將之前的尾節點指向新節點
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    //5. 連結串列長度增加
    size++;
    modCount++;
}

注意點:
boolean add(E e) 新增成功返回true,新增失敗返回false.我們在程式碼中沒有看到有返回false的情況啊,直接在程式碼中寫了個返回true,什麼判斷條件都沒有,之前我們說過連結串列的資料儲存不需要連續的空間儲存,所以只要是還能給它分配空間,就不會新增失敗.當空間不夠分配時(記憶體溢位),會丟擲OutOfMemory。

5.2 addLast(E e)
//新增元素到末尾. 內部實現和add(E e)一樣
public void addLast(E e) {
    linkLast(e);
}
5.3 addFirst(E e)
public void addFirst(E e) {
    linkFirst(e);
}
/**
 1. 新增元素到連結串列頭部
*/
private void linkFirst(E e) {
    //1. 記錄頭結點
    final Node<E> f = first;
    //2. 建立新節點  next指標指向之前的頭結點
    final Node<E> newNode = new Node<>(null, e, f);
    //3. 新建的節點就是頭節點了
    first = newNode;
    //4. 判斷之前連結串列是否為空  
    //為空則將新節點賦給尾節點(相當於空連結串列插入第一個元素,頭結點等於尾節點)
    //非空則將之前的頭結點的prev指標指向新節點
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    //5. 連結串列長度增加
    size++;
    modCount++;
}

解析:
1. 記錄頭結點
2. 構建一個新的節點
3. 將該新節點作為新的頭節點.如果是空連結串列插入第一個元素,那麼頭結點= 尾節點=新節點;如果不是,那麼將之前的頭節點的prev指標指向新節點.
4. 增加連結串列長度
5. 列表內容

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

新增元素到連結串列頭部 這裡的意思比擬壓棧.和pop(出棧:移除連結串列第一個元素)相反.
還記得LinkedList繼承關係嗎?
LinkedList 實現 Deque 介面,push(E e)就是 Deque 介面中的方法。
push(E e)內部實現是和addFirst()一樣的。

5.5 offer(),offerFirst(E e),offerLast(E e)
public boolean offer(E e) {
    return add(e);
}
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

/**
* 新增元素到末尾
*/
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

新增元素到連結串列頭部. 內部實現其實就是add(e)

5.6 add(int index, E element)
//新增元素到指定位置
public void add(int index, E element) {
    //1. 越界檢查
    checkPositionIndex(index);

    //2. 判斷一下index大小
    //如果是和list大小一樣,那麼就插入到最後
    //否則插入到index處
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

//檢查是否越界
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
* Returns the (non-null) Node at the specified element index.
返回指定元素索引處的(非空)節點。
*/
Node<E> node(int index) {
    // assert isElementIndex(index);

    /**
    * 這裡的思想非常巧妙,如果index在連結串列的前半部分,那麼從first開始往後查詢
    否則,從last往前面查詢,節省查詢時間
    */
    //1. 如果index<size/2 ,即index在連結串列的前半部分
    if (index < (size >> 1)) {
        //2. 記錄下第一個節點
        Node<E> x = first;
        //3. 迴圈從第一個節點開始往後查,直到到達index處,返回index處的元素
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        //index在連結串列的後半部分
        //4. 記錄下最後一個節點
        Node<E> x = last;
        //5. 迴圈從最後一個節點開始往前查,直到到達index處,返回index處的元素
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
/**
* Links e as last element.
將e連結到list最後一個元素
*/
void linkLast(E e) {
    //1. 記錄最後一個元素l
    final Node<E> l = last;
    //2. 構建一個新節點,資料為e,前一個是l,後一個是null
    final Node<E> newNode = new Node<>(l, e, null);
    //3. 現在新節點是最後一個元素了,所以需要記錄下來
    last = newNode;
    //4. 如果之前list為空,那麼first=last=newNode,只有一個元素
    if (l == null)
        first = newNode;
    else
        //5. 非空的話,那麼將之前的最後一個指向新的節點
        l.next = newNode;
    //6. 連結串列長度+1
    size++;
    modCount++;
}

/**
* Inserts element e before non-null Node succ.
在非null節點succ之前插入元素e。
*/
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //1. 記錄succ的前一個節點
    final Node<E> pred = succ.prev;
    //2. 構建一個新節點,資料是e,前一個節點是pred,下一個節點是succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    //3. 將新節點作為succ的前一個節點
    succ.prev = newNode;
    //4. 判斷pred是否為空
    //如果為空,那麼說明succ是之前的頭節點,現在新節點在succ的前面,所以新節點是頭節點
    if (pred == null)
        first = newNode;
    else
        //5. succ的前一個節點不是空的話,那麼直接將succ的前一個節點指向新節點就可以了
        pred.next = newNode;
    //6. 連結串列長度+1
    size++;
    modCount++;
}

6 刪除元素的各種方法

6.1 remove() removeFirst()

移除連結串列第一個元素

/**
* 移除連結串列第一個節點
*/
public E remove() {
    return removeFirst();
}

/**
* 移除連結串列第一個節點
*/
public E removeFirst() {
    final Node<E> f = first;
    //注意:如果之前是空連結串列,移除是要報錯的喲
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

/**
* Unlinks non-null first node f.
* 將第一個節點刪掉
*/
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //1. 記錄第一個節點的資料值
    final E element = f.item;
    //2. 記錄下一個節點
    final Node<E> next = f.next;
    //3. 將第一個節點置空  幫助GC回收
    f.item = null;
    f.next = null; // help GC
    //4. 記錄頭節點
    first = next;
    //5. 如果下一個節點為空,那麼連結串列無節點了    如果不為空,將頭節點的prev指標置為空
    if (next == null)
        last = null;
    else
        next.prev = null;
    //6. 連結串列長度-1
    size--;
    modCount++;
    //7. 返回刪除的節點的資料值
    return element;
}

解析:將第一個節點移除並置空,然後將第二個節點作為頭節點.思路還是非常清晰的,主要是對細節的處理.

6.2 remove(int index)

移除指定位置元素

//移除指定位置元素
public E remove(int index) {
    //檢查入參是否合法
    checkElementIndex(index);
    //node(index)找到index處的節點  
    return unlink(node(index));
}

//移除節點x
E unlink(Node<E> x) {
    // assert x != null;
    //1. 記錄該節點資料值,前一個節點prev,後一個節點next
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    //2. 判斷前一個節點是否為空
    if (prev == null) {
        //為空的話,那麼說明之前x節點是頭節點  這時x的下一個節點成為頭節點
        first = next;
    } else {
        //非空的話,將前一個節點的next指標指向x的下一個節點
        prev.next = next;
        //x的prev置為null
        x.prev = null;
    }

    //3. 判斷x後一個節點是否為空
    if (next == null) {
        //為空的話,那麼說明之前x節點是尾節點,這時x的前一個節點成為尾節點
        last = prev;
    } else {
        //為空的話,將x的下一個節點的prev指標指向prev(x的前一個節點)
        next.prev = prev;
        //x的next指標置空
        x.next = null;
    }

    //4. x節點資料值置空
    x.item = null;
    //5. 連結串列長度-1
    size--;
    modCount++;
    //6. 將x節點的資料值返回
    return element;
}

解析:

  1. 首先找到index索引處的節點(這樣就可以方便的獲取該節點的前後節點),記為x 。
  2. 記錄x的前(prev)後(next)節點 。
  3. 將x的前一個節點prev節點的next指標指向next,將x節點的後一個節點的
    prev指標指向prev節點。
  4. 將x節點置空,連結串列長度-1。
6.3 remove(Object o) removeFirstOccurrence(Object o)

從此連結串列中刪除第一次出現的指定元素o

public boolean remove(Object o) {
    //1. 判斷o是否為空
    if (o == null) {
        //為null  迴圈,找第一個資料值為null的節點
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                //刪除該節點
                unlink(x);
                return true;
            }
        }
    } else {
        //非空  迴圈,找第一個與o的資料值相等的節點
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                //刪除該節點
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
//從此連結串列中刪除第一次出現的指定元素o. 內部其實就是上面的remove(o);
public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}

解析:
1. 首先判斷入參是否為null
2. 如果為null,那麼迴圈遍歷連結串列,從頭節點開始往後查詢,找到第一個節點的資料值為null的,直接刪除該節點.
3. 如果非null,那麼迴圈遍歷連結串列,從頭節點開始往後查詢,找到第一個節點的資料值為o的,直接刪除該節點.

6.4 removeLast()

移除最後一個元素並返回

public E removeLast() {
    final Node<E> l = last;
    //如果連結串列是空的,那麼就要丟擲一個錯誤
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
/**
 1. Unlinks non-null last node l.
移除連結串列最後一個元素
*/
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;

    //1. 記錄尾節點資料值
    final E element = l.item;
    //2. 找到尾節點的前一個節點prev
    final Node<E> prev = l.prev;
    //3. 將尾節點置空  方便GC
    l.item = null;
    l.prev = null; // help GC
    //4. 將last賦值為prev  
    last = prev;
    //5. 判斷prev是否為null
    //為空的話,說明之前連結串列就只有1個節點,現在刪了之後,頭節點和尾節點都為null了
    //非空,直接將新任尾節點的next指標指向null
    if (prev == null)
        first = null;
    else
        prev.next = null;
    //6. 連結串列長度-1
    size--;
    modCount++;
    //7. 返回之前尾節點資料值
    return element;
}

解析:
1. 判斷連結串列是否有節點, 沒有節點直接拋錯誤….
2. 首先找到倒數第二個節點(可能沒有哈,沒有的話,說明連結串列只有一個節點)prev
3. 然後將尾節點置空,prev的next指標指向null
4. 連結串列長度-1, 返回之前尾節點資料值

6.5 removeLastOccurrence(Object o)

從此連結串列中刪除最後一次出現的指定元素o.其實和上面的remove(o)是一樣的,只不過這裡遍歷時是從尾節點開始往前查詢的.

public boolean removeLastOccurrence(Object o) {
    if (o == null) {
       //為null  迴圈,從後向前 找第一個資料值為null的節點
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                //刪除該節點
                unlink(x);
                return true;
            }
        }
    } else {
        //不為null  迴圈,從後向前 找第一個資料值為null的節點
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                //刪除該節點
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
6.6 poll() pop()
//獲取第一個元素的同時刪除第一個元素,當連結串列無節點時,不會報錯. 這裡的unlinkFirst()上面已分析過.
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

//獲取第一個元素的同時刪除第一個元素,當連結串列無節點時,會報錯.
public E pop() {
    return removeFirst();
}
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
7 修改元素 set(int index, E element)

首先找到index處節點,替換該節點資料值

設定index處節點資料值為element
public E set(int index, E element) {
    //1. 入參檢測
    checkElementIndex(index);
    //2. 找到index處節點,上面已分析該方法
    Node<E> x = node(index);
    //3. 儲存該節點舊值
    E oldVal = x.item;
    //4. 替換為新值
    x.item = element;
    //5. 將舊值返回
    return oldVal;
}
8 查詢元素
8.1 element() getFirst()
//獲取連結串列第一個元素. 方法比較簡單,就是將連結串列頭節點資料值進行返回
public E element() {
    return getFirst();
}
/ 獲取連結串列第一個元素. 非常簡單,就是將first的資料值返回
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
8.2 get(int index)
//獲取指定索引處元素. 方法比較簡單,就是通過node(index)找到index索引處節點,然後返回其資料值
public E get(int index) {
    //1. 入參檢測
    checkElementIndex(index);
    //2. 獲取指定索引處節點資料值
    return node(index).item;
}
8.3 getLast()
//獲取連結串列最後一個元素. 非常簡單,就是將last的資料值返回
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}