Java集合——LinkedList原始碼分析
一 前言
上一篇我們介紹了ArrayList原始碼解析有想看的同學可以點選這個連結ArrayList原始碼解析。平時我們或多或少都用過LinKedList,但是對其原理不是很瞭解,我們就來一起學習吧。
二 原始碼解析
1. LinkedList概述
LinkedList是一個實現了List介面和Deque介面的雙端連結串列。
有關索引的操作可能從連結串列頭開始遍歷到連結串列尾部,也可能從尾部遍歷到連結串列頭部,這取決於看索引更靠近哪一端。
LinkedList不是執行緒安全的,如果想使LinkedList變成執行緒安全的,可以使用如下方式:
List list=Collections.synchronizedList(new LinkedList(... ));
- LinkedList底層是雙向連結串列儲存資料,並且記錄了頭節點和尾節點
- 插入和刪除比較快(O(1)),查詢則相對慢一些(O(n))。
- 刪除也是非常快,只需要改動一下指標就行了,代價很小.
- 新增元素非常快,如果是新增到頭部和尾部的話更快,因為已經記錄了頭節點和尾節點,只需要連結一下就行了. 如果是新增到連結串列的中間部分的話,那麼多一步操作,需要先找到新增索引處的元素(因為需要連結到這裡),才能進行新增.
- 因為是連結串列結構,所以分配的空間不要求是連續的
- 執行緒不安全
- iterator()和listIterator()返回的迭代器都遵循fail-fast機制。
- 遍歷的時候,建議採用forEach()進行遍歷,這樣可以在每次獲取下一個元素時都非常輕鬆(next = next.next;). 然後如果是通過fori和get(i)的方式進行遍歷的話,效率是極低的,每次get(i)都需要從最前面(或者最後面)開始往後查詢i索引處的元素,效率很低.
LinkedList底層是連結串列結構,說具體點他是雙向迴圈連結串列。什麼是雙向連結串列呢?
雙向連結串列的每個節點包含以下資料:上一個節點的指標,自己的資料,下一個節點的指標.尾節點沒有下一個節點,所以指向null.這樣的結構,比如我拿到連結串列中間的一個節點,即可以往前遍歷,也可以往後遍歷.
2 LinkedList繼承關係
先上一盤菜
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- LinkedList繼承於AbstractSequentialList,AbstractSequentialList這個類提供了List的一個骨架實現介面,以儘量減少實現此介面所需的工作量由“順序訪問”資料儲存(如連結列表)支援。對於隨機訪問資料(如陣列),應使用AbstractList優先於此類。
- LinkedList 實現了List介面,意味著LinkedList元素是有序的,可以重複的,可以有null元素的集合。
- LinkedList 實現 Deque 介面,Deque是Queue的子介面,Queue是一種佇列形式,而Deque是雙向佇列,它支援從兩個端點方向檢索和插入元素。
- LinkedList 實現了Cloneable介面,即覆蓋了函式clone(),能克隆,可以被複制.注意,LinkedList裡面的clone()複製其實是淺複製。
- LinkedList 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸。
3 LinkerList 全域性變數
LinkedList本身的的屬性比較少,主要有三個:
- size 當前有多少個節點。
- first 代表第一個節點。
- last 代表最後一個節點。
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;
}
這個 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是連結串列的節點,Node是LinkedList的靜態內部類,資料結構也比較簡單,如下:
1. item 該節點的資料。
2. next 指向下一個節點的指標。
3. prev 指向上一個節點的指標。
4 構造方法
/**
* 構造一個空列表
*/
public LinkedList() {
}
/**
* 構造列表通過指定的集合
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList一共有2個構造方法,第一個構造一個空列表,第二個構造列表通過指定的集合。我們主要看第二個,
//將指定集合的所有元素插入到末尾位置
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//將指定集合的所有元素插入到index位置
public boolean addAll(int index, Collection<? extends E> c) {
//1. 入參合法性檢查
checkPositionIndex(index);
//2. 將集合轉成陣列
Object[] a = c.toArray();
//3. 記錄需要插入的集合元素個數
int numNew = a.length;
//4. 如果個數為0,那麼插入失敗,不繼續執行了
if (numNew == 0)
return false;
//5. 判斷一下index與size是否相等
//相等則插入到連結串列末尾
//不相等則插入到連結串列中間 index處
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
//找到index索引處節點 這樣就可以方便的拿到該節點的前後節點資訊
succ = node(index);
//記錄index索引處節點前一個節點
pred = succ.prev;
}
//6. 迴圈將集合中所有元素連線到pred後面
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.next = newNode;
pred = newNode;
}
//7. 判斷succ是否為空
//為空的話,那麼集合的最後一個元素就是尾節點
//非空的話,那麼將succ連線到集合的最後一個元素後面
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//8. 連結串列長度+numNew
size += numNew;
modCount++;
return true;
}
解析:addAll 這段程式碼分為了2種情況,一個是原來的連結串列是空的,一個是原來的連結串列有值。我們這邊是構造方法是第一種情況。
- 將需要新增的集合轉成陣列a。
- 判斷需要插入的位置index是否等於連結串列長度size,如果相等則插入到連結串列最後;如果不相等,則插入到連結串列中間,還需要找到index處節點succ,方便拿到該節點的前後節點資訊.
- 記錄index索引處節點的前一個節點pred,迴圈將集合中所有元素連線到pred的後面
- 將集合最後一個元素的next指標指向succ,將succ的prev指標指向集合的最後一個元素
5 新增元素各種方法
5.1 add(E e)
/**
* 新增指定元素到連結串列尾部
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.將e新增到尾部
*/
void linkLast(E e) {
//1. 暫記尾節點
final Node<E> l = last;
//2. 構建節點 前一個節點是之前的尾節點
final Node<E> newNode = new Node<>(l, e, null);
//3. 新建的節點是尾節點了
last = newNode;
//4. 判斷之前連結串列是否為空
//為空則將新節點賦給頭結點(相當於空連結串列插入第一個元素,頭結點等於尾節點)
//非空則將之前的尾節點指向新節點
if (l == null)
first = newNode;
else
l.next = newNode;
//5. 連結串列長度增加
size++;
modCount++;
}
注意點:
boolean add(E e) 新增成功返回true,新增失敗返回false.我們在程式碼中沒有看到有返回false的情況啊,直接在程式碼中寫了個返回true,什麼判斷條件都沒有,之前我們說過連結串列的資料儲存不需要連續的空間儲存,所以只要是還能給它分配空間,就不會新增失敗.當空間不夠分配時(記憶體溢位),會丟擲OutOfMemory。
5.2 addLast(E e)
//新增元素到末尾. 內部實現和add(E e)一樣
public void addLast(E e) {
linkLast(e);
}
5.3 addFirst(E e)
public void addFirst(E e) {
linkFirst(e);
}
/**
1. 新增元素到連結串列頭部
*/
private void linkFirst(E e) {
//1. 記錄頭結點
final Node<E> f = first;
//2. 建立新節點 next指標指向之前的頭結點
final Node<E> newNode = new Node<>(null, e, f);
//3. 新建的節點就是頭節點了
first = newNode;
//4. 判斷之前連結串列是否為空
//為空則將新節點賦給尾節點(相當於空連結串列插入第一個元素,頭結點等於尾節點)
//非空則將之前的頭結點的prev指標指向新節點
if (f == null)
last = newNode;
else
f.prev = newNode;
//5. 連結串列長度增加
size++;
modCount++;
}
解析:
1. 記錄頭結點
2. 構建一個新的節點
3. 將該新節點作為新的頭節點.如果是空連結串列插入第一個元素,那麼頭結點= 尾節點=新節點;如果不是,那麼將之前的頭節點的prev指標指向新節點.
4. 增加連結串列長度
5. 列表內容
5.4 push(E e)
public void push(E e) {
addFirst(e);
}
新增元素到連結串列頭部 這裡的意思比擬壓棧.和pop(出棧:移除連結串列第一個元素)相反.
還記得LinkedList繼承關係嗎?
LinkedList 實現 Deque 介面,push(E e)就是 Deque 介面中的方法。
push(E e)內部實現是和addFirst()一樣的。
5.5 offer(),offerFirst(E e),offerLast(E e)
public boolean offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* 新增元素到末尾
*/
public boolean offerLast(E e) {
addLast(e);
return true;
}
新增元素到連結串列頭部. 內部實現其實就是add(e)
5.6 add(int index, E element)
//新增元素到指定位置
public void add(int index, E element) {
//1. 越界檢查
checkPositionIndex(index);
//2. 判斷一下index大小
//如果是和list大小一樣,那麼就插入到最後
//否則插入到index處
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//檢查是否越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Returns the (non-null) Node at the specified element index.
返回指定元素索引處的(非空)節點。
*/
Node<E> node(int index) {
// assert isElementIndex(index);
/**
* 這裡的思想非常巧妙,如果index在連結串列的前半部分,那麼從first開始往後查詢
否則,從last往前面查詢,節省查詢時間
*/
//1. 如果index<size/2 ,即index在連結串列的前半部分
if (index < (size >> 1)) {
//2. 記錄下第一個節點
Node<E> x = first;
//3. 迴圈從第一個節點開始往後查,直到到達index處,返回index處的元素
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//index在連結串列的後半部分
//4. 記錄下最後一個節點
Node<E> x = last;
//5. 迴圈從最後一個節點開始往前查,直到到達index處,返回index處的元素
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Links e as last element.
將e連結到list最後一個元素
*/
void linkLast(E e) {
//1. 記錄最後一個元素l
final Node<E> l = last;
//2. 構建一個新節點,資料為e,前一個是l,後一個是null
final Node<E> newNode = new Node<>(l, e, null);
//3. 現在新節點是最後一個元素了,所以需要記錄下來
last = newNode;
//4. 如果之前list為空,那麼first=last=newNode,只有一個元素
if (l == null)
first = newNode;
else
//5. 非空的話,那麼將之前的最後一個指向新的節點
l.next = newNode;
//6. 連結串列長度+1
size++;
modCount++;
}
/**
* Inserts element e before non-null Node succ.
在非null節點succ之前插入元素e。
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//1. 記錄succ的前一個節點
final Node<E> pred = succ.prev;
//2. 構建一個新節點,資料是e,前一個節點是pred,下一個節點是succ
final Node<E> newNode = new Node<>(pred, e, succ);
//3. 將新節點作為succ的前一個節點
succ.prev = newNode;
//4. 判斷pred是否為空
//如果為空,那麼說明succ是之前的頭節點,現在新節點在succ的前面,所以新節點是頭節點
if (pred == null)
first = newNode;
else
//5. succ的前一個節點不是空的話,那麼直接將succ的前一個節點指向新節點就可以了
pred.next = newNode;
//6. 連結串列長度+1
size++;
modCount++;
}
6 刪除元素的各種方法
6.1 remove() removeFirst()
移除連結串列第一個元素
/**
* 移除連結串列第一個節點
*/
public E remove() {
return removeFirst();
}
/**
* 移除連結串列第一個節點
*/
public E removeFirst() {
final Node<E> f = first;
//注意:如果之前是空連結串列,移除是要報錯的喲
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* Unlinks non-null first node f.
* 將第一個節點刪掉
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//1. 記錄第一個節點的資料值
final E element = f.item;
//2. 記錄下一個節點
final Node<E> next = f.next;
//3. 將第一個節點置空 幫助GC回收
f.item = null;
f.next = null; // help GC
//4. 記錄頭節點
first = next;
//5. 如果下一個節點為空,那麼連結串列無節點了 如果不為空,將頭節點的prev指標置為空
if (next == null)
last = null;
else
next.prev = null;
//6. 連結串列長度-1
size--;
modCount++;
//7. 返回刪除的節點的資料值
return element;
}
解析:將第一個節點移除並置空,然後將第二個節點作為頭節點.思路還是非常清晰的,主要是對細節的處理.
6.2 remove(int index)
移除指定位置元素
//移除指定位置元素
public E remove(int index) {
//檢查入參是否合法
checkElementIndex(index);
//node(index)找到index處的節點
return unlink(node(index));
}
//移除節點x
E unlink(Node<E> x) {
// assert x != null;
//1. 記錄該節點資料值,前一個節點prev,後一個節點next
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//2. 判斷前一個節點是否為空
if (prev == null) {
//為空的話,那麼說明之前x節點是頭節點 這時x的下一個節點成為頭節點
first = next;
} else {
//非空的話,將前一個節點的next指標指向x的下一個節點
prev.next = next;
//x的prev置為null
x.prev = null;
}
//3. 判斷x後一個節點是否為空
if (next == null) {
//為空的話,那麼說明之前x節點是尾節點,這時x的前一個節點成為尾節點
last = prev;
} else {
//為空的話,將x的下一個節點的prev指標指向prev(x的前一個節點)
next.prev = prev;
//x的next指標置空
x.next = null;
}
//4. x節點資料值置空
x.item = null;
//5. 連結串列長度-1
size--;
modCount++;
//6. 將x節點的資料值返回
return element;
}
解析:
- 首先找到index索引處的節點(這樣就可以方便的獲取該節點的前後節點),記為x 。
- 記錄x的前(prev)後(next)節點 。
- 將x的前一個節點prev節點的next指標指向next,將x節點的後一個節點的
prev指標指向prev節點。 - 將x節點置空,連結串列長度-1。
6.3 remove(Object o) removeFirstOccurrence(Object o)
從此連結串列中刪除第一次出現的指定元素o
public boolean remove(Object o) {
//1. 判斷o是否為空
if (o == null) {
//為null 迴圈,找第一個資料值為null的節點
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//刪除該節點
unlink(x);
return true;
}
}
} else {
//非空 迴圈,找第一個與o的資料值相等的節點
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
//刪除該節點
unlink(x);
return true;
}
}
}
return false;
}
//從此連結串列中刪除第一次出現的指定元素o. 內部其實就是上面的remove(o);
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
解析:
1. 首先判斷入參是否為null
2. 如果為null,那麼迴圈遍歷連結串列,從頭節點開始往後查詢,找到第一個節點的資料值為null的,直接刪除該節點.
3. 如果非null,那麼迴圈遍歷連結串列,從頭節點開始往後查詢,找到第一個節點的資料值為o的,直接刪除該節點.
6.4 removeLast()
移除最後一個元素並返回
public E removeLast() {
final Node<E> l = last;
//如果連結串列是空的,那麼就要丟擲一個錯誤
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
/**
1. Unlinks non-null last node l.
移除連結串列最後一個元素
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
//1. 記錄尾節點資料值
final E element = l.item;
//2. 找到尾節點的前一個節點prev
final Node<E> prev = l.prev;
//3. 將尾節點置空 方便GC
l.item = null;
l.prev = null; // help GC
//4. 將last賦值為prev
last = prev;
//5. 判斷prev是否為null
//為空的話,說明之前連結串列就只有1個節點,現在刪了之後,頭節點和尾節點都為null了
//非空,直接將新任尾節點的next指標指向null
if (prev == null)
first = null;
else
prev.next = null;
//6. 連結串列長度-1
size--;
modCount++;
//7. 返回之前尾節點資料值
return element;
}
解析:
1. 判斷連結串列是否有節點, 沒有節點直接拋錯誤….
2. 首先找到倒數第二個節點(可能沒有哈,沒有的話,說明連結串列只有一個節點)prev
3. 然後將尾節點置空,prev的next指標指向null
4. 連結串列長度-1, 返回之前尾節點資料值
6.5 removeLastOccurrence(Object o)
從此連結串列中刪除最後一次出現的指定元素o.其實和上面的remove(o)是一樣的,只不過這裡遍歷時是從尾節點開始往前查詢的.
public boolean removeLastOccurrence(Object o) {
if (o == null) {
//為null 迴圈,從後向前 找第一個資料值為null的節點
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
//刪除該節點
unlink(x);
return true;
}
}
} else {
//不為null 迴圈,從後向前 找第一個資料值為null的節點
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
//刪除該節點
unlink(x);
return true;
}
}
}
return false;
}
6.6 poll() pop()
//獲取第一個元素的同時刪除第一個元素,當連結串列無節點時,不會報錯. 這裡的unlinkFirst()上面已分析過.
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//獲取第一個元素的同時刪除第一個元素,當連結串列無節點時,會報錯.
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
7 修改元素 set(int index, E element)
首先找到index處節點,替換該節點資料值
設定index處節點資料值為element
public E set(int index, E element) {
//1. 入參檢測
checkElementIndex(index);
//2. 找到index處節點,上面已分析該方法
Node<E> x = node(index);
//3. 儲存該節點舊值
E oldVal = x.item;
//4. 替換為新值
x.item = element;
//5. 將舊值返回
return oldVal;
}
8 查詢元素
8.1 element() getFirst()
//獲取連結串列第一個元素. 方法比較簡單,就是將連結串列頭節點資料值進行返回
public E element() {
return getFirst();
}
/ 獲取連結串列第一個元素. 非常簡單,就是將first的資料值返回
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
8.2 get(int index)
//獲取指定索引處元素. 方法比較簡單,就是通過node(index)找到index索引處節點,然後返回其資料值
public E get(int index) {
//1. 入參檢測
checkElementIndex(index);
//2. 獲取指定索引處節點資料值
return node(index).item;
}
8.3 getLast()
//獲取連結串列最後一個元素. 非常簡單,就是將last的資料值返回
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}