前言:面試時被問到 LinkedList 的底層實現,瞬間啞口無言,所以決定狠狠地啃一下 Java源碼,目標就是吊打面試官!
好了,廢話不多說,進入正題。
簡單介紹
很多人聽到 LinkedList,應該就能猜到它的底層是用鏈表實現的,更準確來說,是用 雙向鏈表 實現的,和常用的 ArrayList 不同,LinkedList 更適用於插入和刪除操作較多的場景,而且LinkedList 實現了 Queue 接口,所以經常被當做隊列來使用。
那麼 LinkedList 底層到底是怎麼實現的呢?
LinkedList 的成員變量
俗話說,學習一個類,就要先從這個類包含的屬性入手,那麼我們就來看看吧!
LinkedList 的主要變量:
transient int size = 0; // 雙向鏈表的節點數量
transient Node<E> first; // 雙向鏈表的第一個節點
transient Node<E> last; // 雙向鏈表的最後一個節點
另外,LinkedList 內部結構是雙向鏈表,作爲鏈表的組成,必然少不了節點類 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;
}
}
注意,看到這裏我們就要思考一下,LinkedList 的主要操作是怎麼實現的?
我們不妨來大膽猜想一下:
add(E e)
:將指定元素添加到鏈表末尾。讓鏈表的最後一個節點的 next 指向新增節點 e,讓新增節點的 prev 指向最後一個節點,最後將 last 指向新增節點,size+1。get(int index)
:返回鏈表中指定位置的元素。檢查下標是否越界,如果越界,拋異常;沒越界,則從頭或者從尾部開始遍歷(指定節點離哪個近就從哪開始),找到指定節點後返回節點值。peek()
:檢索但不刪除此鏈表的第一個元素。若 first 爲空,返回 null,否則返回 first 節點值。poll()
:檢索並刪除此鏈表的第一個元素。若 first 爲空,返回 null,否則返回 first 節點值,並刪除 frist 節點。remove(Object o)
:從鏈表中刪除第一次出現的指定元素。若 o 爲空,遍歷鏈表找到值爲 null 的節點並刪除;否則,遍歷鏈表找到值等於 o 的節點值 的節點,並刪除。size()
:返回鏈表的元素數量。直接返回 size。
--------------------------------------------- 想狂虐面試官?請往下分析源碼 ---------------------------------------------
LinkedList 的主要方法
接下來將分析 LinkedList 幾個主要方法的源碼,不要緊張,可以先放鬆一下,深吸一口氣,其實閱讀源碼並沒有那麼難,只要掌握正確的方法和節奏,看我來簡單地分析給你聽:
1.add 方法
有多種不同的add()方法,我們這裏分析一下比較常用的 add(E e):
注:代碼中的中文註釋是筆者添加的
public boolean add(E e) {
// 將 e 添加到鏈表尾部
linkLast(e);
return true;
}
void linkLast(E e) {
// 尾節點
final Node<E> l = last;
// 新增節點初始化,prev 指向 尾節點
final Node<E> newNode = new Node<>(l, e, null);
// last 指向 新增節點
last = newNode;
// 舊鏈表尾節點 爲空
if (l == null)
first = newNode;
// 不爲空
else
l.next = newNode;
// 鏈表節點數量 + 1
size++;
modCount++;
}
總結:
調用 add(E e) 方法,先初始化該新增節點,並將 prev 指向舊鏈表的最後一個節點,若舊鏈表最後一個節點爲空(即鏈表爲空),first 和 last 都指向新增節點,最後讓 size +1,鏈表操作次數 modCount+1。
2.get 方法
LinkedList 中的 get 方法爲 get(int index) ,那我們就來看一下源碼:
public E get(int index) {
// 檢查下標是否越界,越界則拋異常
checkElementIndex(index);
// 獲取指定下標節點的節點值
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
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;
}
}
總結:
調用 get(int index) 方法,先判斷下標 index 是否越界,越界則拋異常;沒越界則根據下標選擇從頭節點或者從尾節點開始遍歷,找到指定下標節點,則返回該節點的節點值。
3.poll 方法
peek() 方法和 poll() 方法都是檢索鏈表的頭節點,但是 poll() 檢索後要刪除該節點,比較複雜,所以我們只分析 poll()。
public E poll() {
// 頭節點爲空,返回 null, 否則刪除頭節點並返回
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 刪除頭節點並返回
private E unlinkFirst(Node<E> f) {
// 獲取頭節點元素值
final E element = f.item;
// 獲取第二個節點
final Node<E> next = f.next;
// 頭節點元素值 置空
f.item = null;
// 頭節點 next 置空, 後期 JVM 會進行 GC 回收
f.next = null; // help GC
// first 指向 第二個節點
first = next;
// 第二個節點爲空
if (next == null)
last = null;
// 第二個節點不爲空
else
next.prev = null;
// 鏈表節點數量 + 1
size--;
modCount++;
return element;
}
總結:
調用 poll() 方法,先判斷頭節點是否爲空,爲空則返回 null;不爲空則將 first 指向 第二個節點後,返回頭節點的值,並刪除頭節點。
4.remove 方法
LinkedList 的 remove 方法也有很多個,我們來分析常用的 remove(Object o) 方法(刪除第一次出現指定元素的節點)。
public boolean remove(Object o) {
// 若 o 爲空
if (o == null) {
// 遍歷找到值爲 null 的節點並刪除
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
// 若 o 不爲空
} else {
// 遍歷找到值爲 x的值 的節點並刪除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//刪除 x 節點
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;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
總結:
調用 remove(Object o),若 o 爲空,則遍歷鏈表找到值爲 null 的節點並刪除;若 o 不爲空,則遍歷鏈表找到值爲 o的值 的節點並刪除。
分析結束啦!謝謝觀看~