Java填坑系列之LinkedList

金空空發表於2019-04-02

Java填坑系列之LinkedList

前言

今天我們通過分析LinkedList的原始碼,來學習一下它內部是如何新增、查詢以及刪除元素的。同時在閱讀原始碼時,也要思考以下幾個問題。

  • LinkedList的底層資料結構是什麼?
  • 與ArrayList有什麼區別?
  • LinkedList是執行緒安全的嗎?

繼承關係

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
  //......
}
複製程式碼

首先我們來看一下LinkedList的繼承關係,可以看出它繼承自AbstractSequentialList。並且實現List、Deque、Cloneable以及Serializable介面,然後我們再來看一下它的核心欄位。

核心欄位

//表示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);
}
複製程式碼

可以看出構造方法比較簡單,我們再來看一下Node類。

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節點類是LinkedList中一個私有的靜態內部類,包含元素、前節點和後節點。

新增

新增一個節點

public boolean add(E e) {
        linkLast(e);
        return true;
    }
 void linkLast(E e) {
 	//l為尾節點
        final Node<E> l = last;
        //建立一個以l節點為前節點,資料為e,後節點為null的Node節點,此時newNode為尾節點
        final Node<E> newNode = new Node<>(l, e, null);
        //更新尾節點
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
複製程式碼

從原始碼中可以看出LinkedList新增一個元素是從連結串列尾部進行新增,新增步驟如下:

  • 將尾節點賦值l節點
  • 建立一個以l節點為前節點,資料為e,後節點為null的Node節點
  • 更新尾節點
  • 判斷l節點是否為空
  • size++、modCount++

通過指定index新增

public void add(int index, E element) {
  	//判斷index是否越界
        checkPositionIndex(index);
        if (index == size)
           //此時表示在連結串列尾部新增
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
  void linkBefore(E e, Node<E> succ) {
        // succ表示index對應的節點,pred表示succ前節點
        final Node<E> pred = succ.prev;
        //newNode的前節點為pred,後節點為succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        //succ的前節點為newNode
        succ.prev = newNode;
        //如果pred為null,說明succ原來是頭結點,而現在succ的前節點為newNode,所以現在頭結點是newNode
        if (pred == null)
            first = newNode;
        else
           //pred的後節點為newNode
            pred.next = newNode;
        size++;
        modCount++;
    }
複製程式碼

修改

首先我們來看通過指定索引來修改Node資料,原始碼如下

public E set(int index, E element) {
   //檢查是否陣列越界
    checkElementIndex(index);
    //通過node方法來獲得對應得Node節點
    Node<E> x = node(index);
    //儲存舊資料
    E oldVal = x.item;
    //賦值新資料
    x.item = element;
    //返回舊資料
    return oldVal;
    }
複製程式碼

可以看出修改資料主要分為以下幾步:

  • 獲得index對應的Node節點
  • 儲存Node節點舊資料
  • 賦值新資料

獲取

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
複製程式碼

可以看出get()方法內部只有兩行程式碼,我們分別來看一下都是做了什麼操作。

checkElementIndex(index)

 private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
 private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }
複製程式碼

可以看出checkElementIndex()方法主要是來判斷index是否陣列越界,如果越界就丟擲對應的異常。

node(index)

Node<E> node(int 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;
        }
    }
複製程式碼

可以看出node()方法通過判斷index是處於前半段還是後半段,來查詢對應的Node節點。通過折半查詢提升了一定的效率。

刪除

刪除指定索引對應的節點

  public E remove(int index) {
        checkElementIndex(index);
        //傳入index對應的Node節點
        return unlink(node(index));
    }
   E unlink(Node<E> x) {
        //獲取Node節點資料
        final E element = x.item;
        //獲取後節點
        final Node<E> next = x.next;
        //獲取前節點
        final Node<E> prev = x.prev;
	//分別對前節點和後節點進行了判斷
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
	//將節點資料置為null
        x.item = null;
        size--;
        modCount++;
        return element;
    }
複製程式碼

然後我們再來簡單看下LinkedList中其他remove()方法,具體如下:

 //刪除第一個節點資料
 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);
    }
複製程式碼

總結

通過分析LinkedList的原始碼,我們可以知道LinkedList在插入和刪除上有著比較大的優勢,這也符合連結串列的特性。而且通過判斷index在前半段還是後半段,使用折半查詢的方法來獲得對應的Node節點,提升了一定的效率。

參考資料

面試必備:LinkedList

相關文章