Java容器之LinkedList源碼分析(LinkedList到底是單鏈表還是雙鏈表?)

  前面在分析ArrayListVector容器的源碼時,發現的底層實現原理都是維護一個數組,並且自動調整數組的大小(擴容、縮小),隨機查找效率高,但是插入刪除操作效率低。在此篇博客中,博主將帶領各位小夥伴也看看LinkedList容器的實現原理,它又有什麼優勢呢,它到底是單鏈表還是雙鏈表實現呢?

註明:以下源碼分析都是基於jdk 1.8.0_221版本
在這裏插入圖片描述

一、LinkedList容器概述

  LinkedList類的申明如下:

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

在這裏插入圖片描述
  Java中的LinkedList容器既實現了List接口,又實現了Deque接口,所以LinkedList即可當做List容器使用,又可以當做雙端隊列使用。ArrayListVector容器底層通過維護一個數組存放數據,而LinkedList容器通過維護一個雙鏈表存放數據。
在這裏插入圖片描述

二、LinkedList類中的主要屬性與內部類

1、Node內部類

  NodeLinkedList容器中的鏈表節點。

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;
    }
}

2、LinkedList類中的主要屬性

/**
 * 鏈表的長度
 */
transient int size = 0;

/**
 * 指向鏈表的頭結點
 */
transient Node<E> first;

/**
 * 指向鏈表的尾結點
 */
transient Node<E> last;

三、LinkedList類的構造方法

  由於LinkedList類的屬性比較簡單,所以構造器也沒啥要初始化的。

public LinkedList() {
}

/**
 * 複製構造器
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

四、查找相關的方法

1、get方法

/**
 * 通過下標獲取容器中的元素
 */
public E get(int index) {
	// 檢查下標的合法性[0, size)
	// 肯定有不少小夥伴蒙圈了,不是說LinkedList是鏈表麼,怎麼能按小標取值呢?把表頭看成下標0,對應的index只要往後移動index此即可
    checkElementIndex(index);
    return node(index).item;
}

/**
 * 通過下標取出鏈表中的節點
 */
Node<E> node(int index) {
    // 如果 index < size / 2,則從first頭結點開始往後移動
    if (index < (size >> 1)) {
        Node<E> x = first;
        // first看做下標0對應的位置
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
    	// 否則從尾節點last,從後往前移動
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

五、插入相關的方法

1、add方法

/**
 * 從尾端插入元素
 */
public boolean add(E e) {
	// 調用linkLast插入尾端
    linkLast(e);
    return true;
}

/**
 * 將元素插入尾端
 */
void linkLast(E e) {
    final Node<E> l = last;
    // Node(pre指向,當前元素,next指向)
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾指針指向新插入的節點
    last = newNode;
    // 如果l(之前的last)爲null,說明之前的鏈表爲空,更新first,否則更新l的next指向newNode
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    // 插入元素(結構性調整),此變量在AbstractList類中定義
    modCount++;
}

/**
 * 指定插入的下標
 */
public void add(int index, E element) {
	// 檢查下標是否合法
    checkPositionIndex(index);

    if (index == size)
    	// 下標 == size,表示插入尾端
        linkLast(element);
    else
    	// 否則插入node(index)的前面
        linkBefore(element, node(index));
}
/**
 * 插入指定節點的前面
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // Node(新建節點pre指向,新建元素,新建節點next指向)
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 然後需要將succ.prev節點.next指向e,succ節點的pre指向e
    succ.prev = newNode;
    // 如果succ.prev == null,表示succ爲頭結點,而e是插在succ的前面,此時e爲頭節點
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

六、刪除相關的方法

1、remove方法

/**
 * 刪除指定下標的節點
 */
public E remove(int index) {
	// 檢查下標合法性[0, size)
    checkElementIndex(index);
    // 調用unlink方法,將節點node(index)移除鏈表
    return unlink(node(index));
}
/**
 * 將節點x移出鏈表
 */
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    // 記錄節點x的prev指向節點、next指向節點
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    
    if (prev == null) {
    	// 節點x沒有prev,說明當前節點是頭結點
        first = next;
    } else {
    	// 否則更新prev.next指向x.next
        prev.next = next;
        // 釋放x的prev
        x.prev = null;
    }

    if (next == null) {
    	// 節點x沒有next,說明當前節點是尾結點
        last = prev;
    } else {
    	// 否則更新next.prev指向x.prev
        next.prev = prev;
        // 釋放x的next
        x.next = null;
    }
	// 釋放x指向的節點
    x.item = null;
    size--;
    modCount++;
    return element;
}

七、其它方法

  LinkedList實現了Deque雙端隊列接口(頭、尾部都可以進行出隊、入隊)。

/**
 * 返回頭部節點
 */
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

/**
 * 頭部節點出隊
 */
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}



/**
 * 返回頭部節點
 */
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
 }

/**
 * 返回尾部節點
 */
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

/**
 * 移除頭部節點
 */
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

/**
 * 移除尾部節點
 */
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

八、總結

  LinkedList容器底層實現用的是鏈表,所以節點插入、刪除只需要改變指針的指向,並不需要複製其它節點,所以效率較高,但是隨機訪問(按下標訪問)需要從頭節點(爲節點)一個個移動,效率比數組訪問低。因此,如果你需要做大量的插入、刪除,可選擇LinkedList容器,如果容器的元素基本不變,有大量的隨機訪問操作,可選擇ArrayList容器,如果還需要支持併發讀寫,可選擇Vector容器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章