LinkedList 源碼分析

前言

上篇文章分析了 ArrayList 的源碼,面試過程當中常常會被面試官來問 LinkedList 和 ArrayList 的區別,這篇文章從源碼的角度來看下 LinkedList 之後,再和上篇文章作個對比,相信你會有一個本身的判斷的。前端

LinkedList 簡介

老規矩,先來看下官方 Api 對 LinkedList 的介紹:java

從圖中能夠看出,LinkedList 和 ArrayList 都是直接或者間接繼承於 AbstractList 的,可是和 ArrayList 不一樣的是 LinkedList 是直接繼承於 AbstractSequentialList 的。node

先來看下這個 AbstractSequentialList :面試

Api 中也描述了 AbstractSequentialList 提供了一個基本的List接口實現,爲實現序列訪問的數據結構存儲提供了所須要的最小化接口實現,而對於支持隨機訪問數據的List好比數組,應該優先使用 AbstractList。編程

和 AbstractList 實現隨機訪問相反,AbstractSequentialList 採用的迭代器實現的 get、set、add 和 remove 等黨閥後端

爲了實現這個列表。僅僅須要拓展這個類,而且提供ListIterator和size方法。 對於不可修改的List,編程人員只須要實現Iterator的hasNext、next和hasPrevious、previous和index方法 對於可修改的List還須要額外實現Iterator的的set的方法 對於大小可變的List,還須要額外的實現Iterator的remove和add方法數組

LinkedList 實現的全部接口有:安全

  • 實現了 Serializable 是序列化接口,所以它支持序列化,可以經過序列化傳輸。
  • 實現了 Cloneable 接口,能被克隆。
  • 實現了Iterable 接口,能夠被迭代器遍歷
  • 實現了 Collection ,擁有集合操做的方法
  • 實現 Deque/Queue 能夠看成隊列/雙端隊列使用
  • 實現了 List 接口,擁有增刪改查等方法

先看下LinkedList 的特色,對 LinkedList 有一個大致上的認識:數據結構

  1. LinkedList 底層數據結構是雙向鏈表,可是頭節點不存放數據,只有後置節點的引用;
  2. 集合中的元素容許爲 null,能夠看到源碼中在查找和刪除時,都劃分爲該元素爲null和不爲null兩種狀況來處理。
  3. 容許內部元素重複
  4. 不存在擴容問題,因此是沒有擴容的方法
  5. 元素在內部是有序存放的,依次在鏈表上添加節點
  6. 實現了棧和隊列的操做方法,所以也能夠做爲棧、隊列和雙端隊列來使用
  7. 因爲是鏈表實現,而且沒有實現RandomAccess ,雖然在查找的時候,會先判斷是在前半部分或者後半部分,而後依次從前或者從後查找,可是查找效率仍是很低,不過增刪效率高,可是查找和修改大部分狀況下不如 ArrayList。
  8. 線程不安全,能夠用個 Collections.SynchronizedList(new LinkedList()) 返回一個線程安全的 LinkedList

下面從源碼的角度進行分析:dom

LinkedList 源碼分析

一些屬性

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;
    // 序列化ID
    private static final long serialVersionUID = 876323262645176354L;
}
複製代碼

前面講了,LinkedList 是基於雙向鏈表實現的,因此屬性也很簡單,定義了 大小、頭節點和尾節點。

看下每一個節點的結構:

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;
    }
}
複製代碼

很明顯的雙向鏈表的結構。

構造方法

// 空的構造方法,什麼都沒作,只是生成了對象
public LinkedList() {
}
// 傳入了集合 c,並將其插入到鏈表中。
public LinkedList(Collection<? extends E> c) {
    this();
  	// 添加方法稍後分析
    addAll(c);
}
複製代碼

構造方法也很簡單,沒有什麼特殊的操做。

前面講了,LinkedList 能夠當作一個 List 使用,也能夠當作隊列使用,依次進行分析:

做爲列表使用的一些方法:

添加(add)的一些方法

先看下 add 方法:

// 這個方法實現的效果和 addLast(E e) 是同樣的
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 顧名思義,連接到最後。也就是把添加的元素添加到尾節點。
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++;
}
複製代碼

add(int index, E element)

在指定位置添加

public void add(int index, E element) {
  	//檢查插入位置的合法性,便是否比 0 大,比當前的 size 小
    checkPositionIndex(index);
  	// 若是是等於當前大小,就是至關於在尾部再插入一個節點
  	// 不然就是插入到 index 所在位置的節點的前面
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

// 返回指定索引處的一個非空節點 
// 這裏是 LinkedList 作的一個優化,先判斷索引是在前半部分和後半部分
// 若是前半部分,從頭節點開始找,正序找
// 若是後半部分,從尾節點開始找,倒序找
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;
    }
}

// 插入到指定節點的前面
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
  	// 取出查找到指定位置的節點
    final Node<E> pred = succ.prev;
  	// 構建新節點,前置節點找到節點的原前置節點,e 是元素值,後置節點是根據位置找到的 succ
    final Node<E> newNode = new Node<>(pred, e, succ);
  	// 原位置的前置節點設置爲要插入的節點。
    succ.prev = newNode;
  	// 若是原位置的前置節點爲空,即原位置 succ 是頭節點,即 add(0 ,E )而後把新建節點賦值爲頭節點。
    if (pred == null)
        first = newNode;
    else
    // 不爲空,原位置的前置節點的後置節點設置爲新節點。
        pred.next = newNode;
    size++;
    modCount++;
}
複製代碼

總的來講就是: 先檢查是否在可插入的範圍內,不在拋異常,若是 index 和當前 size 相等,直接插入到尾節點,若是小於當前 size,那麼就插入到 index 節點的前面。

看下 addAll

// 沒有傳入位置,直接加到最後
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

// 加入到指定位置
public boolean addAll(int index, Collection<? extends E> c) {
  	// 檢查 index 合法性
    checkPositionIndex(index);
		// 傳入的 Collection 轉換成數組
    Object[] a = c.toArray();
    int numNew = a.length;
		// 空數組,直接返回插入失敗
    if (numNew == 0)
        return false;
		// pred 是 succ 的前置節點 ,succ指向當前須要插入節點的位置的節點
    Node<E> pred, succ;
  	// index 等於 size,尾插
  	// 不等於,找到須要插入位置的節點,以及其前置節點,pred 可能爲空
    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 後置節點設置爲新節點
            pred.next = newNode;
      	// 每次設置完,pred 表示剛插入的節點,依次日後插入
        pred = newNode;
    }

  	// 若是是從 size 位置開始添加,最後添加的節點成了尾節點
    if (succ == null) {
        last = pred;
    } else {
    // 若是不是從 size 開始添加,數組中最後一個元素的後置節點指向爲 原 index 位置節點
    // 原 index 位置節點的前置節點置爲數組中最後一個元素構建的節點。
        pred.next = succ;
        succ.prev = pred;
    }
    size += numNew;
    modCount++;
    return true;
}
複製代碼

addFirst 、addLast

// 添加元素到鏈表頭。
public void addFirst(E e) {
    linkFirst(e);
}
// 添加元素到鏈表尾
public void addLast(E e) {
    linkLast(e);
}
複製代碼

linkLast 前面在講 add 的時候已經分析過了,再來看下 linkFirst

private void linkFirst(E e) {
  	// 保存頭節點
    final Node<E> f = first;
  	// 建立新節點
    final Node<E> newNode = new Node<>(null, e, f);
  	// 頭節點設置爲新節點
    first = newNode;
  	// 若是頭節點爲空,表名鏈表爲裏面沒數據,尾節點也須要設置爲新節點
    if (f == null)
        last = newNode;
    else
    // 頭節點不爲空,設置原頭節點的前置節點爲 新節點。
        f.prev = newNode;
    size++;
    modCount++;
}
複製代碼

刪除(remove)的一些方法

//移除指定位置的元素
public E remove(int index) {
    checkElementIndex(index);
  	// 先拿到指定位置的節點
    return unlink(node(index));
}

// 移除指定元素
// 這裏和 ArrayList 裏面移除比較類似,分爲 null 和 不爲 null 兩種狀況。先從頭節點遍歷找到要移除的元素,
// 而後執行移除第一個元素對應的節點的操做。。
// 是移除第一個相等的元素!
// 是移除第一個相等的元素!
// 是移除第一個相等的元素!
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) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
  	// 前置節點爲 null ,代表要移除頭節點,把下一個節點設置爲頭節點
  	// 前置節點不爲 null ,x 的前置節點的後置節點指向 x 的後置節點
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
  	// 後置節點爲 null ,代表要移除尾節點,把上一個節點設置爲尾節點
  	// 後置節點不爲 null ,x 的後置節點的前置節點指向 x 的前置節點
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
  	// 釋放引用的元素,gc 可回收
    x.item = null;
    size--;
    modCount++;
    return element;
}
複製代碼

這兩個刪除的方法基本都是先找到要刪除元素對應的節點,而後再去執行 unlink 方法去對節點的 前置節點、後置節點進行從新指向。而後把引用的元素 置爲 null ,便於 gc 回收移除的元素,最後返回移除元素。

此外還有移除第一個和最後一個

// 移除第一個元素
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);
}

// 移除第一個元素,調整指針指向,並把頭部部元素置空,便於 gc 回收
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;
}
// 移除最後一個元素,調整指針指向,並把尾部元素置空,便於 gc 回收
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;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}
複製代碼

修改(set)的一些方法

// 修改一個元素
public E set(int index, E element) {
  	// 檢查index 是否在合法位置
    checkElementIndex(index);
  	// 經過 node 函數找到要修改位置對應的節點
    Node<E> x = node(index);
  	// 而後直接修改元素裏面的 item 屬性,完成修改
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
複製代碼

查找(get)的一些方法

// 查找指定位置的元素
public E get(int index) {
	  // 檢查index 是否在合法位置
    checkElementIndex(index);
  	// 經過 node 函數找到要修改位置對應的節點,並返回其 item 屬性,即爲元素值。
    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;
}
複製代碼

上面的都比較簡單。

清除(clear) 的一些方法

// 移除列表中的全部元素
public void clear() {
    // Clearing all of the links between nodes is "unnecessary", but:
    // - helps a generational GC if the discarded nodes inhabit
    // more than one generation
    // - is sure to free memory even if there is a reachable Iterator
  
  	// 遍歷全部的不爲 null 的節點,把全部數據所有置爲 null,便於gc 回收
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}
複製代碼

做爲隊列使用的一些方法

隊列是什麼?

隊列是一種比較特殊的線性結構。它只容許在表的前端(front)進行刪除操做,而在表的後端(rear)進行插入操做。進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊頭。

隊列中最早插入的元素也將最早被刪除,對應的最後插入的元素將最後被刪除。所以隊列又稱爲「先進先出」(FIFO—first in first out)的線性表,與棧(FILO-first in last out)恰好相反。

隊列的抽象是 Queue,而 LinkedList 是實現了 Deque 接口的,Deque 又繼承了 Queue,因此LinkedList 是能夠當作隊列來使用的。

先看下 Queue 接口:

public interface Queue<E> extends Collection<E> {
  	// 增長一個元素到隊列尾部,若是隊列有大小限制,而且隊列已滿,會拋出異常 IllegalArgumentException
    boolean add(E e);

  	// 增長一個元素到隊列尾部,和 add 不一樣的是:若是隊列有大小限制,而且隊列已滿,則返回 false,不拋出異常
    boolean offer(E e);
  	
  	// 檢索到隊列頭部元素,而且將其移出隊列。和 poll 方法不一樣的是若是隊列是空的,那麼拋出 NoSuchElementException 異常
    E remove();

  	// 檢索到隊列頭部元素,而且將其移出隊列。若是隊列是空的,那麼返回 null;
    E poll();
  
  	// 檢索隊列頭部的元素,並不會移除,和 peek 方法不一樣的是:若是隊列是空的,那麼拋出 NoSuchElementException 異常;
    E element();

  	// 檢索隊列頭部的元素,並不會移除,若是隊列是空的,那麼返回 null;
    E peek();
}
複製代碼

LinkedList 裏面的實現

add 、offer

add 前面已經分析過了,這裏來看下 offer:

public boolean offer(E e) {
    return add(e);
}
複製代碼

前面隊列中的定義已經寫了,在 add 會在隊列滿的時候拋出異常,可是這個發現 offer 方法也調用的 add 方法,因此只是對 add 的一種包裝,實際使用效果是同樣的。這是爲何呢?

這是由於前面註釋裏面講了,add 添加的時候拋出異常只會在隊列大小有限制的狀況,在LinkedList 中並無設置大小的地方,因此也就不存在超過隊列大小的限制了。

remove 、poll
public E remove() {
    return removeFirst();
}

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

同理,這兩個也不會拋出異常。

remove 會直接調用 removeFirst 從頭部移除元素,而且在 removeFirst 方法移除的過程當中可能會拋出異常。

poll 則先把頭部元素取出來,進行判空。

若是爲空,則返回 null,什麼都不作,不會拋出異常,直接返回 null;

若是不爲空,那麼就執行 unlinkFirst(f) ,這個 unlinkFirst 前面已經講了,把頭部元素移除。

element、peek();
public E element() {
    return getFirst();
}

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

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

和上面註釋的同樣,兩個都是取頭部元素,element 會拋出異常,peek 只是取頭部元素,不會拋出異常。

做爲雙向隊列使用的一些方法

雙向隊列是隊列 (Queue) 的一個子接口 Deque,雙向隊列兩端的元素均可以入隊列和出隊列。能夠實現先進先出或者先進後出的數據結構。

若是把 Deque 限制爲只能從一端入隊和出隊,那麼就能夠實現 的結構數據結構,遵循 先進後出 的規則。

若是不對 Deque 進行限制,用做雙向隊列,那麼就是先進新出。

主要方法以下:

public interface Deque<E> extends Queue<E> {
    // 將指定元素插入此雙端隊列的開頭(若是能夠直接這樣作而不違反容量限制)
    void addFirst(E e);

    //將指定元素插入此雙端隊列的末尾(若是能夠直接這樣作而不違反容量限制)。
    void addLast(E e);

    //在不違反容量限制的狀況下,將指定的元素插入此雙端隊列的開頭。
    boolean offerFirst(E e);

    // 在不違反容量限制的狀況下,將指定的元素插入此雙端隊列的末尾。
    boolean offerLast(E e);

    // 獲取並移除此雙端隊列第一個元素。
    E removeFirst();

    // 獲取並移除此雙端隊列的最後一個元素。
    E removeLast();

    //獲取並移除此雙端隊列的第一個元素;若是此雙端隊列爲空,則返回 null。
    E pollFirst();

    //獲取並移除此雙端隊列的最後一個元素;若是此雙端隊列爲空,則返回 null。
    E pollLast();

    // 獲取,但不移除此雙端隊列的第一個元素。
    E getFirst();

    // 獲取,但不移除此雙端隊列的最後一個元素。
    E getLast();

    // 獲取,但不移除此雙端隊列的第一個元素;若是此雙端隊列爲空,則返回 null。
    E peekFirst();

    //獲取,但不移除此雙端隊列的最後一個元素;若是此雙端隊列爲空,則返回 null。
    E peekLast();

    //今後雙端隊列移除第一次出現的指定元素。
    boolean removeFirstOccurrence(Object o);

    // 今後雙端隊列移除最後一次出現的指定元素。
    boolean removeLastOccurrence(Object o);

    // *** Queue methods ***

    // 將指定元素插入此雙端隊列所表示的隊列(換句話說,此雙端隊列的尾部),若是能夠直接這樣作而不違反容量限制的話;
  	// 若是成功,則返回 true,若是當前沒有可用空間,則拋出 IllegalStateException。
    boolean add(E e);

    // 將指定元素插入此雙端隊列所表示的隊列(換句話說,此雙端隊列的尾部),若是能夠直接這樣作而不違反容量限制的話;
  	// 若是成功,則返回 true,若是當前沒有可用的空間,則返回 false。
    boolean offer(E e);

    //獲取並移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素)。
    E remove();

    //獲取並移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素);若是此雙端隊列爲空,則返回 null。
    E poll();

    //獲取,但不移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素)。
    E element();

    //獲取,但不移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素);若是此雙端隊列爲空,則返回 null。
    E peek();


    // *** Stack methods ***

    // 將一個元素推入此雙端隊列所表示的堆棧(換句話說,此雙端隊列的頭部),若是能夠直接這樣作而不違反容量限制的話;
  	// 若是成功,則返回 true,若是當前沒有可用空間,則拋出 IllegalStateException。
    void push(E e);

    // 今後雙端隊列所表示的堆棧中彈出一個元素
    E pop();


    // *** Collection methods ***

    // 今後雙端隊列中移除第一次出現的指定元素
    boolean remove(Object o);

    // 是否包含一個元素
    boolean contains(Object o);

    // 隊列大小
    int size();

    // 返回此雙端隊列的迭代器
    Iterator<E> iterator();

    // 返回一個迭代器,該迭代器具備此雙端隊列的相反順序
    Iterator<E> descendingIterator();

}
複製代碼

具體的就不在分析,在 LinkedList 無非是比隊列多一些操做而已,有興趣的能夠去看下關於雙端隊列相關的部分源碼。

總結

  1. LinkedList 底層數據結構是雙向鏈表,可是頭節點不存放數據,只有後置節點的引用;
  2. 集合中的元素容許爲 null,能夠看到源碼中在查找和刪除時,都劃分爲該元素爲null和不爲null兩種狀況來處理。
  3. 容許內部元素重複
  4. 不存在擴容問題,因此是沒有擴容的方法
  5. 元素在內部是有序存放的,依次在鏈表上添加節點
  6. 實現了棧和隊列的操做方法,所以也能夠做爲棧、隊列和雙端隊列來使用
  7. 因爲是鏈表實現,而且沒有實現RandomAccess ,雖然在查找的時候,會先判斷是在前半部分或者後半部分,而後依次從前或者從後查找,可是查找效率仍是很低,不過增刪效率高,可是查找和修改大部分狀況下不如 ArrayList。
  8. 線程不安全,能夠用個 Collections.SynchronizedList(new LinkedList()) 返回一個線程安全的 LinkedList

大多數狀況下使用 ArrayList 便可,其餘的仍是根據具體的業務場景根據二者的不一樣特性進行不一樣的選擇。

關於 ArrayList 和 LinkedList 的性能對比,能夠參考這篇文章

相關文章
相關標籤/搜索