集合框架之LinkedList
源碼分析
一、簡述
LinkedList
是Java集合中一個全能的結構,底層採用了雙向鏈表
結構。和ArrayList
也支持空值和重複值,但是使用鏈表實現,在插入元素效率高,但是遍歷查詢效率比較低。
LinkedList
是非線程安全的集合類,併發環境,多線程操作LinkedList
,會引發不可預知的錯誤。
優點:底層是鏈表,所以增刪只需要移動指針,效率很高,不需要批量擴容和預留空間。
缺點:隨機訪問需要從前到後(從後到前)遍歷,隨着節點增加,效率一直降低。
可以使用List list=Collections.synchronizedList(new LinkedList(...));
將LinkedList
變成線程安全的。
二、繼承結構
從UML
圖中我們可以很明顯看到LinkedList
實現了Deque
接口這意味着我們可以把LinkedList
當做隊列來使用(隊列的一種實現方式)。
基本的結構圖:
三、成員變量和節點
成員變量:
transient int size = 0; // 大小size
// 這裏的fist指針需要整個過程中滿足,(first == null && last == null) || (first.prev == null && first.item != null)
transient Node<E> first; // 指向第一個節點的指針。
// 這裏的last指針需要整個過程中滿足,(first == null && last == null) || (last.next == null && last.item != null)
transient Node<E> last; // 指向最後一個節點的指針
節點:
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;
}
}
四、構造函數
無參構造:空的List
public LinkedList() {}
有參構造:構造一個包含指定集合的元素的列表,其順序由集合的迭代器返回。
public LinkedList(Collection<? extends E> c) {
this();
// 把集合中所有節點添加到list中, 源碼詳解請看下面的解釋
addAll(c);
}
接着我們看一下addAll
方法:看5.1.4
五、核心方法
5.1. 增加相關方法
5.1.1. add(E e)
:在鏈表尾部添加元素有兩種情況:
- 鏈表中沒有元素,
add
添加元素 - 鏈表中有元素,
add
添加元素
/**
* 增加一個指定的到鏈表的末尾
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e 被插入到鏈表元素
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
// 保存尾指針
final Node<E> l = last;
// 創建新節點,節點的pre指針last, next指向null
final Node<E> newNode = new Node<>(l, e, null);
// 將last指針指向新建的節點
last = newNode;
// 對應的頭指針移動到新節點上
if (l == null)
first = newNode;
else
// 將尾指針指向新節點
l.next = newNode;
// 數量增加
size++;
// 修改次數增加
modCount++;
}
上述代碼流程的梳理:
-
使用臨時變量
L
來保存尾節點; -
創建新節點
new Node<>(l, e, null)
,將新節點的prev
指針指向尾節點,將新節點next
節點指向null
。Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }
-
將
last
尾指針指向新節點 -
這是是分爲兩種情況,在空的鏈表上添加新節點;在非空鏈表上添加新節點
i. 對於第一種情況:
l==null
(就是頭指針和尾指針都指向null
),需要將first
頭指針也指向新節點。ii. 對於第二種情況:
l.next = newNode
,需要將尾指針的next
指向新節點,這就是可以將雙向鏈表連接起來 -
最後增加鏈表節點數量增加一,操作修改數量增加一
5.1.2. add(int index, E element)
在指定位置添加節點
這裏面也有兩種情況需要考慮:
- 增加元素的位置是頭尾的情況
- 增加元素的位置是非頭尾的情況
/**
* 將指定的元素插入此列表中的指定位置。
* 將當前在該位置的元素(如果有的話)和任何後繼元素右移(將其索引加一)。
*
* @param index 指定元素要插入的索引
* @param element 要插入的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
// 判斷是index大於size
checkPositionIndex(index);
// 如果index等於size
if (index == size)
// 在鏈表節點尾部添加元素,這個我們上面已經分析了
linkLast(element);
else
// 在指定位置添加元素
linkBefore(element, node(index));
}
/**
* 返回指定元素索引處的(非空)節點。
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// index位於size的前半部分,則從頭節點開始遍歷,返回index位置的節點
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// index位於size的後半部分,則從尾節點開始遍歷,返回index位置的節點
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* 在非null節點succ之前插入元素e。
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 使用pred保存index位置節點的prev指向元素
final Node<E> pred = succ.prev;
// 將新節點的next指向index位置所在的節點
// 將新節點的prev指向index位置所在的節點prev的指向
final Node<E> newNode = new Node<>(pred, e, succ);
// 再將index位置的節點prev指向新節點
succ.prev = newNode;
// 如果實在鏈表頭插入,則需要將first指針指向新節點
if (pred == null)
first = newNode;
else
// 否則將就pred指向元素的next指向新節點
pred.next = newNode;
// 節點數量增加
size++;
// 修改次數增加
modCount++;
}
上述代碼流程的梳理:
- 先要檢測index是否合法
index >= 0 && index <= size
- 判斷是否是在鏈表尾添加元素,是的話直接調用我們上面分析的
linkLast(element)
方法 - 否則就需要先獲取
index
位置的節點,然後調用linkBefore(element, node(index))
此方法。 - 進入此方法之後,首先是先要使用一個臨時變量去保存
index
位置的節點prev
保存的前一個節點。 - 創建一個新節點,將新節點的
next
指向index
位置所在的節點,將新節點的prev
指向臨時變量(這個臨時變量保存的是就是index
位置節點prev
指向的前一個節點)。 - 接着就將
index
位置節點的prev
指向剛纔新創建的節點 - 然後就要判斷是在表頭添加(
pred == null
),如果是隻需要將first
指針指向新節點就可以;否者,就需要將臨時變量(這個臨時變量保存的是就是index
位置節點prev
指向的前一個節點)的next
指向新節點。 - 最後將節點的數量++,修改次數++。
5.1.3. addFirst(E e)
和addLast(E e)
在鏈表頭尾添加元素
/**
* 將指定的元素插入此列表的開頭。
*
* @param e 要添加的元素
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* 將e鏈接爲第一個元素。
*/
private void linkFirst(E e) {
// 先用一個臨時變量f保存first指針
final Node<E> f = first;
// 創建一個新節點,將新節點的prev指向爲null,將next指向臨時變量f
final Node<E> newNode = new Node<>(null, e, f);
// 將fist指針指向新創建的節點
first = newNode;
// 這裏需要判斷f是否是null,如果是則說明當前鏈表的空鏈表,就需要把last指針也指向新節點
if (f == null)
last = newNode;
else
// 否則將f變量的prev指向新節點。這樣就把鏈表就又串起來了
f.prev = newNode;
size++;
modCount++;
}
在表尾添加元素是一樣的不展開分析了!
5.1.4. addAll(int index, Collection<? extends E> c)
將一個集合添加到鏈表中指定位置
/**
* 從指定位置開始,將指定集合中的所有元素插入此鏈表。
* 將當前位於該位置的元素(如果有)和任何後續元素右移(增加其索引)。
* 新元素將按指定集合的迭代器返回的順序顯示在列表中。
*
* @param index 從指定集合中插入第一個元素的索引
* @param c 包含要添加到此列表的元素的集合
* @return {@code true} if this list changed as a result of the call
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 判斷index位置是否合適
checkPositionIndex(index);
// 將集合轉成數組,並獲取數組的長度
Object[] a = c.toArray();
int numNew = a.length;
// 數組長度爲0,則返回false
if (numNew == 0)
return false;
Node<E> pred, succ;
// 這裏判斷是在鏈表的中間位置插入集合元素還是在鏈表的尾部添加元素,並將臨時指針指向指定位置
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 通過迭代器將數組一一添加到集合中
for (Object o : a) {
// @SuppressWarnings 屏蔽某些編譯時的警告信息,此處用來屏蔽強制類型轉換
@SuppressWarnings("unchecked") E e = (E) o;
// 新建節點,將指向前一個元素指針賦值給新節點的prev指針,新節點的next指針指向向null
Node<E> newNode = new Node<>(pred, e, null);
// 此處用來應對在鏈表頭位置添加元素的情況
if (pred == null)
first = newNode;
else
// 將上一個元素的next指針指向新節點
pred.next = newNode;
// 移動last指針指到最新的元素
pred = newNode;
}
// 將原來的index位置的節點連接到現在的鏈表上
if (succ == null) {
// 對於鏈表尾添加集合元素,直接將last指針移動到最後就可以。
last = pred;
} else {
// 對於在鏈表中間或者鏈表頭添加集合,將最後一個新建節點的next指向succ(index
// 位置的節點),將succ(index位置節點指向前一個節點的指針指向pred節點)
pred.next = succ;
succ.prev = pred;
}
// 鏈表數量修改
size += numNew;
// 修改次數加一
modCount++;
return true;
}
5.2. 刪除節點相關方法
5.2.1. remove()
刪除第一個節點
這個方法調用unlinkFirst()
核心方法
/**
* 檢索並刪除此鏈表的頭(第一個元素)。
*
* @return 此鏈表的頭
* @throws NoSuchElementException if this list is empty
* @since 1.5
*/
public E remove() {
return removeFirst();
}
/**
* 從此鏈表中刪除並返回第一個元素。
*
* @return 此鏈表中的第一個元素
* @throws NoSuchElementException 鏈表是空
*/
public E removeFirst() {
final Node<E> f = first;
// 如果first指針是null,那說明整個鏈表是空的,會拋出NoSuchElementException異常
if (f == null)
throw new NoSuchElementException();
// 調用
return unlinkFirst(f);
}
/**
* 這個是鏈表刪的核心方法
* 移除鏈接非空的第一個節點f。
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 保存要刪除節點的數據
final E element = f.item;
// 獲取頭節點的下一個節點信息
final Node<E> next = f.next;
// 將頭節點置空
f.item = null;
f.next = null; // help GC
// 將頭指針指向下一個節點
first = next;
// 如果next指針是null,就說明是空鏈表,則將last節點也置空
if (next == null)
last = null;
else
// 否則將新的頭節點的前指針指向null
next.prev = null;
size--;
modCount++;
return element;
}
代碼流程描述:
- 先獲取
first
指針,判斷是否爲null
,空鏈拋出異常; - 調用
unlinkFirst(Node<E> f)
方法 - 通過臨時變量先保存
first
指向的數據,並保存first
指向的next
下一個節點 - 接着將頭節點置空
- 判斷頭節點指向的下一個節點是否爲
null
,如果爲null
的話則說明是空鏈,將last
指針指向null
;否者將頭節點的前置指針指向null
; - 最後將節點數量減一,修改次數減一,並返回第三步保存的數據。
此外刪除,最後一個節點的方法與此方法相識,不做分析。
5.2.2. 刪除指定位置的元素
其中remove(int index)
、remove(Object o)
、removeFirstOccurrence(Object o)移除第一齣現的元素
、removeLastOccurrence(Object o)移除最後一次出現的元素
這幾個方法都是調用了unlink(Node<E> x)
方法。
接下來我們看一下這個方法unlink(Node<E> x)
:
/**
* 移除鏈接非空節點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;
// 前置指針爲空,(說明這個位置是位於頭節點),直接將頭指針指向next
if (prev == null) {
first = next;
} else {
// 否則將前一個節點的後指針指向next節點,斷開當前節點與前一節點的聯繫
prev.next = next;
x.prev = null;
}
// 後置指針爲空,(說明這個位置是位於尾節點),直接將尾節點指向prev
if (next == null) {
last = prev;
} else {
// 否則將後一個節點的前置指針指向prev節點,斷開當前節點與後一節點的聯繫
next.prev = prev;
x.next = null;
}
// 將元素爲kong
x.item = null;
size--;
modCount++;
return element;
}
5.3. 獲取某一個位置的節點
5.3.1. indexOf(Object o)
獲取首次出現元素的位置,否者返回-1
/**
* 返回指定元素在此列表中首次出現的索引,如果此列表不包含該元素,則返回-1。
*
* @param o 搜索元素
* @return 指定的元素在此列表中第一次出現的索引;如果此列表不包含該元素,則爲-1
*/
public int indexOf(Object o) {
// 初始爲0
int index = 0;
// 判斷元素是否爲null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 如果沒有獲取到則爲 -1
return -1;
}
5.3.2. lastIndexOf(Object o)
:獲取最後出現元素的位置,否者返回-1
這個方法與上面的方法是反方向查詢,不做詳細註釋。
/**
* 返回指定元素在此列表中最後一次出現的索引;如果此列表不包含該元素,則返回-1。
*
* @param o 搜索元素
* @return 指定的元素在此列表中最後一次出現的索引;如果此列表不包含該元素,則爲-1
*/
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
5.4. 一些其他的方法
5.4.1 toArray()
按順序將節點放到數組中
public Object[] toArray() {
// 先創造出一個size大小的數組
Object[] result = new Object[size];
int i = 0;
// 遍歷將元素一個一個放進去
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
5.4.2. 序列化方法
LinkedList
中重寫了writeObject
方法,ObjectOutputStream
中將調用ObjectStreamClass
裏的方法通過反射根據方法名稱來調用writeObject
方法,以LinkedList
中定義的方式來序列化鏈表中的元素和size
字段。
將LinkedList
寫入流(序列化)
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out size
s.writeInt(size);
// Write out all elements in the proper order.
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
將LinkedList
從流中讀出來(反序列化)
/**
* Reconstitutes this {@code LinkedList} instance from a stream
* (that is, deserializes it).
*/
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
5.4.3. 克隆方法
/**
* 返回此{@code LinkedList}的淺表副本。 (元素本身未被克隆。)
*
* @return 此{@code LinkedList}實例的淺表副本
*/
public Object clone() {
// 直接調用系統的clone方法構建一個LinkedList對象
LinkedList<E> clone = superClone();
// 爲新的克隆的元素置於鏈表的初始狀態
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
// 將這些元素在試用add方法加入鏈表中
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
六、迭代器的使用
這LinkedList
中使用迭代器或者增強for
循環是比使用原始for
塊很多的。
6.1. 迭代器的變量
// 本次遍歷返回的元素節點
private Node<E> lastReturned;
// LinkedList中下一個元素的指針
private Node<E> next;
// 下一個元素的索引,每遍歷一個元素,該值加1
private int nextIndex;
// 當前創建迭代器時list的修改次數
private int expectedModCount = modCount;
6.2. 迭代器的初始化
// 重指定位置開始迭代, 根據index的值,給next和nextIndex賦初始值
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
6.3. 迭代器一些簡單方法
// 判斷是否仍然有元素, 如果nextIndex 大於 size的時候,就說明已經遍歷到最後一個元素
public boolean hasNext() {
return nextIndex < size;
}
// 判斷當前位置的上一個位置是否有元素
public boolean hasPrevious() {
return nextIndex > 0;
}
// 獲取下一個遍歷元素的下標(索引)
public int nextIndex() {
return nextIndex;
}
// 獲取上一個元素的下標(索引)
public int previousIndex() {
return nextIndex - 1;
}
6.4. 迭代器在遍歷的時候核心方法
這個迭代器是可以對所遍歷的集合進行修改的,提供了add
、set
、remove
方法。這裏我們需要記住:避免併發修改異常的原則:在迭代過程中,不允許利用非迭代器的API
對集合進行更新操作。
先看一下next
方法
public E next() {
// 先檢查是否發生併發修改的異常, 這裏的expectedModCount保存的在使用迭代器最後修改的次數,當併發修改的時候,是無法再次更新到迭代器內部的最後修改次數,所以到expectedModCount和modCount是不一樣的,就是發生了併發修改
checkForComodification();
// 檢測是否包含下一個元素
if (!hasNext())
throw new NoSuchElementException();
// 把當前遍歷的next節點,賦值給lastReturned,最後會將其返回
lastReturned = next;
// 更新下一個節點的位置
next = next.next;
// 然後將索引下標加一
nextIndex++;
return lastReturned.item;
}
接下來看一下遍歷過程中修改的方法
// 利用迭代器對象移除一個節點,但是不能再創建迭代器之後就立即使用remove方法,需要在執行next()方法之後纔可以
public void remove() {
// 先檢查是否發生併發修改的異常 ==> fail-fast失效
checkForComodification();
// 當創建迭代器之後,lastReturned是null,直接執行是拋出異常
if (lastReturned == null)
throw new IllegalStateException();
// 獲取lastReturned的下一個節點
Node<E> lastNext = lastReturned.next;
// 移除節點
unlink(lastReturned);
// 這裏的可能出現多線程操作迭代器,在移除節點之後其他的線程已經操作了next的位置,這裏就是判斷當前迭代器對象的next是否與lastReturned是指向同一個元素,同一個元素的話只需要將next指向lastNext就可以,否則需要將索引減一
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
// 檢測當期迭代的節點是否爲null,爲null說明迭代對象已經到尾
if (lastReturned == null)
throw new IllegalStateException();
// 接着檢查是否發生併發修改的異常
checkForComodification();
// 最後修改元素
lastReturned.item = e;
}
// 這個add的方法是ListIterator接口提供的方法,利用迭代器對象添加一個元素到鏈表中
public void add(E e) {
// 先檢查是否發生併發修改的異常,
checkForComodification();
// 檢測當前迭代器是否爲null,如果是null,則說明已經迭代到了鏈表的尾巴,此時執行add方法將在鏈表的尾追加元素e,否則在next位置,插入元素,這裏兩個方法可以看上面的源碼分析
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
// 然後將索引和迭代器中修改次數加一
nextIndex++;
expectedModCount++;
}
過程看如圖:
七、LinkedList
作爲Queue
使用
7.1. 實現隊列
7.1.1. 隊列簡述
所有的插入只能在表的一端進行,而所有的刪除都在表的另一端進行的線性表。
表中允許插入的一端稱爲隊尾(Rear),允許刪除的一端稱爲隊頭(Front)。按先進先出(FIFO)的原則進行的。
7.1.2. 隊列的常用方法
方法名 | 描述 |
---|---|
add(E e) | 向隊列插入元素,在滿隊列中會拋出異常,不利於判斷,更推薦offer |
element() | 返回隊頭元素,如果隊列爲空,則拋出一個NoSuchElementException 異常 |
offer(E e) | 向隊列插入元素返回true,如果隊列已滿,則返回false |
peek() | 返回隊頭元素 , 如果隊列爲空,則返回null |
poll() | 移除並返問隊頭元素,隊列爲空,則返回null |
remove() | 移除並返回隊頭元素,隊列爲空,則拋出一個NoSuchElementException 異常 |
Deque
繼承了 Queue
接口的方法。當 Deque
當做 隊列使用時(FIFO
),添加元素是添加到隊尾,刪除時刪除的是頭部元素。在Linked
實現的時候我們可以看到
7.1.3. 實例
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.add("張三");
queue.add("李四");
System.out.println("當前隊列元素:" + queue);
System.out.println("獲取隊頭元素:" + queue.element());
queue.offer("王五");
System.out.println("當前隊列元素:" + queue);
System.out.println("獲取隊頭元素:" + queue.peek());
System.out.println("移除隊頭元素:" + queue.poll());
queue.remove();
System.out.println("當前隊列元素:" + queue);
}
7.2. 實現雙端隊列
7.2.1. 雙端對列的簡述:
我們可以再LinkedList
的UML
看到LinkedList
實現了Deque
的雙端隊列。
雙端隊列是限定插入和刪除操作在表的兩端進行的線性表,是一種具有隊列和棧的性質的數據結構。
7.2. 雙端隊列的特點:
雙端隊列中的元素可以從兩端進隊和出隊,其限定插入和刪除操作在鏈表的兩端進行
這裏面的方法都是調用了我們剛纔上面分析的那些方法。
八、LinkedList
作爲Stack
使用
8.1. 棧的簡述
棧”通常是指“後進先出”(LIFO)的容器。有時棧也被稱爲疊加棧,因爲最後壓入棧的元素第一個彈出棧。
LinkedList
具有能夠實現棧的所有功能的方法,因此可以直接將LinkedList
作爲棧使用。
8.2. 棧的實現
class Stack<T> {
private Deque<T> stack = new LinkedList<>();
/**
* 入棧
* @param t
*/
public void push(T t) {
stack.push(t);
}
/**
* 出棧
*/
public void pop() {
stack.pop();
}
/**
* 獲取棧頂元素
* @return
*/
public T peek() {
return stack.peek();
}
/**
* 判空
* @return
*/
public boolean empty() {
return stack.isEmpty();
}
/**
* 獲取棧中元素數量
* @return
*/
public int size() {
return stack.size();
}
}
測試:
public static void main(String[] args) {
Stack<String> stringStack = new Stack<>();
for (String item : "2 0 2 0 鼠 年 快 樂".split(" ")) {
stringStack.push(item);
}
while (!stringStack.empty()) {
System.out.print(stringStack.peek());
stringStack.pop();
}
}
關於隊列和棧部分重寫一篇文章詳細介紹!