前言
今天我們通過分析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節點,提升了一定的效率。