因爲最近工做有點忙,進行了 APP 的部分優化,期間也學習了不少有關於佈局優化和其餘性能優化的知識,可是仍然以爲不太成體系,期待能有更多的優質的性能優化實戰文章可以涌現出來,以便於你們一塊兒交流學習。html
週末有時間把手頭的工做放一放,來繼續進行 Java 集合源碼的學習。今天來學習下 「LinkedList」的源碼。node
先來看下 LinkedList 的繼承體系圖,這裏悄悄告訴你們一個方法在學習源碼的時候如何查看一個類的繼承體系的方法,第一步打開 IntelliJ IDEA 找到你要查看的類 ,第二步點擊右鍵,選擇 Diagrams 選擇二級菜單的任意一項,就能夠獲得下面這樣一個體系圖,還有好多方便的操做,你們能夠經過這篇文章來了解下 使用IntelliJ IDEA查看類的繼承關係圖形 。面試
圖中藍色實線箭頭是指繼承關係 ,綠色虛線箭頭是指接口實現關係。數組
LinkedList
繼承自 AbstrackSequentialList
並實現了 List
接口以及 Deque
雙向隊列接口,所以 LinkedList 不但擁有 List 相關的操做方法,也有隊列的相關操做方法。安全
LinkedList
和 ArrayList
同樣實現了序列化接口 Serializable
和 Cloneable
接口使其擁有了序列化和克隆的特性。性能優化
LinkedList
一些主要特性:bash
LinkedList
集合底層實現的數據結構爲雙向鏈表LinkedList
集合中元素容許爲 nullLinkedList
容許存入重複的數據LinkedList
中元素存放順序爲存入順序。LinkedList
是非線程安全的,若是想保證線程安全的前提下操做 LinkedList
,可使用 List list = Collections.synchronizedList(new LinkedList(...));
來生成一個線程安全的 LinkedList
鏈表是一種不一樣於數組的數據結構,雙向鏈表是鏈表的一種子數據結構,它具備如下的特色:數據結構
每一個節點上有三個字段:當前節點的數據字段(data),指向上一個節點的字段(prev),和指向下一個節點的字段(next)。app
LLink | Data | RLink |
---|
概述上說了雙向鏈表的特色,而 LinkedList
又繼承自 Deque
這個雙鏈表接口,在介紹 LinkedList
的具體方法前咱們先了解下雙向鏈表的實現。框架
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
的節點實現徹底符合雙向鏈表的數據結構要求,而構造方法第一個參數爲上一個節點的索引,當前節點的元素,下一個節點索引。
LinkedList 主要成員變量有下邊三個:
//LinkedList 中的節點個數
transient int size = 0;
//LinkedList 鏈表的第一個節點
transient Node<E> first;
//LinkedList 鏈表的最後一個節點
transient Node<E> last;
複製代碼
之因此 LinkedList 要保存鏈表的第一個節點和最後一個節點是由於,咱們都知道,鏈表數據結構相對於數組結構,有點在於增刪,缺點在於查找。若是咱們保存了LinkedList 的頭尾兩端,當咱們須要以索引來查找節點的時候,咱們能夠根據 index
和 size/2
的大小,來決定從頭查找仍是從尾部查找,這也算是必定程度上彌補了單鏈表數據結構的缺點。
LinkedList 有兩個構造函數:
/**
* 空參數的構造因爲生成一個空鏈表 first = last = null
*/
public LinkedList() {
}
/**
* 傳入一個集合類,來構造一個具備必定元素的 LinkedList 集合
* @param c 其內部的元素將按順序做爲 LinkedList 節點
* @throws NullPointerException 若是 參數 collection 爲空將拋出空指針異常
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
複製代碼
帶參數的構造方法,調用 addAll(c)
這個方法,實際上這方法調用了 addAll(size, c)
方法,在外部單獨調用時,將指定集合的元素做爲節點,添加到 LinkedList
鏈表尾部: 而 addAll(size, c)
能夠將集合元素插入到指定索引節點。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
複製代碼
/**
* 在 index 節點前插入包含全部 c 集合元素的節點。
* 返回值表示是否成功添加了對應的元素.
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 查看索引是否知足 0 =< index =< size 的要求
checkPositionIndex(index);
// 調用對應 Collection 實現類的 toArray 方法將集合轉爲數組
Object[] a = c.toArray();
//檢查數組長度,若是爲 0 則直接返回 false 表示沒有添加任何元素
int numNew = a.length;
if (numNew == 0)
return false;
// 保存 index 當前的節點爲 succ,當前節點的上一個節點爲 pred
Node<E> pred, succ;
// 若是 index = size 表示在鏈表尾部插入
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 遍歷數組將對應的元素包裝成節點添加到鏈表中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
//若是 pred 爲空表示 LinkedList 集合中尚未元素
//生成的第一個節點將做爲頭節點 賦值給 first 成員變量
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
// 若是 index 位置的元素爲 null 則遍歷數組後 pred 所指向的節點即爲新鏈表的末節點,賦值給 last 成員變量
if (succ == null) {
last = pred;
} else {
// 不然將 pred 的 next 索引指向 succ ,succ 的 prev 索引指向 pred
pred.next = succ;
succ.prev = pred;
}
// 更新當前鏈表的長度 size 並返回 true 表示添加成功
size += numNew;
modCount++;
return true;
}
複製代碼
通過上邊的代碼註釋能夠了解到,LinkedList 批量添加節點的方法實現了。大致分下面幾個步驟:
對於 checkPositionIndex
方法這裏想順帶分析了,LinkedList
中有兩個方法用於檢查角標越界,內部實現同樣,都是經過判斷 index >= 0 && index < size
是否知足條件。
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* Tells if the argument is the index of a valid position for an
* iterator or an add operation.
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
複製代碼
LinkedList 做爲鏈表數據結構的實現,不一樣於數組,它能夠方便的在頭尾插入一個節點,而 add 方法默認在鏈表尾部添加節點:
/**
* Inserts the specified element at the beginning of this list.
*
* @param e the element to add
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
*/
public void addLast(E e) {
linkLast(e);
}
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
複製代碼
上述英文太過簡單不翻譯了,咱們能夠看到 add 方法是有返回值的,這個能夠注意下。看來這一系方法都調用用了 linkXXX
方法,
/**
* 添加一個元素在鏈表的頭節點位置
*/
private void linkFirst(E e) {
// 添加元素以前的頭節點
final Node<E> f = first;
//以添加的元素爲節點值構建新的頭節點 並將 next 指針指向 以前的頭節點
final Node<E> newNode = new Node<>(null, e, f);
// first 索引指向將新的節點
first = newNode;
// 若是添加以前鏈表空則新的節點也做爲未節點
if (f == null)
last = newNode;
else
f.prev = newNode;//不然以前頭節點的 prev 指針指向新節點
size++;
modCount++;//操做數++
}
/**
* 在鏈表末尾添加一個節點
*/
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//不然將以前的未節點的 next 指針指向新節點
l.next = newNode;
size++;
modCount++;//操做數++
}
複製代碼
除了上述幾種添加元素的方法,以及以前在將構造的時候說明的 addAll 方法,LinkedList 還提供了 add(int index, E element);
方法,下面咱們來看在這個方法:
/**
* 在指定 index 位置插入節點
*/
public void add(int index, E element) {
// 檢查角標是否越界
checkPositionIndex(index);
// 若是 index = size 表明是在尾部插入節點
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
複製代碼
能夠先看到當 0 =< index <size
的時候調用了 linkBefore(element, node(index))
方法,咱們先來看下 node(index) 方法的實現:
/**
* 返回一個非空節點,這個非空節點位於 index 位置
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// 若是 index < size/2 則從0開始尋找指定角標的節點
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 若是 index >= size/2 則從 size-1 開始尋找指定角標的節點
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
複製代碼
你們可能會疑惑爲何這裏註釋爲返回一個非空節點?其實仔細想下就明白了,這裏的節點必定不爲 null,若是一開始鏈表爲空的時候,index 爲 0 的位置確定爲 null,爲何不會產生異常狀況呢?其實若是一開始鏈表中沒有元素 size = 0,若是咱們向 index = 0
的位置添加元素是不會走到 else 中的,而是會調用 linkLast(element);
方法去添加元素。 所以 node 方法能夠用於根據指定 index 去以 size/2 爲界限搜索index 位置的 Node;
咱們再看回 linkBefore 方法,爲何要叫作 linkBefore 呢,由於在鏈表的中間位置添加節點,其實就是講 index 原來的節點前添加一個節點,添加節點咱們須要知道該節點的前一個節點和當前節點,
linkBefore 也是作了上述四件事:
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 因爲 succ 必定不爲空,因此能夠直接獲取 prev 節點
final Node<E> pred = succ.prev;
// 新節點 prev 節點爲 pred,next 節點爲 succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 原節點的 prev 指向新節點
succ.prev = newNode;
// 若是 pred 爲空即頭節點出插入了一個節點,則將新的節點賦值給 first 索引
if (pred == null)
first = newNode;
else
pred.next = newNode;//不然 pred 的下一個節點改成新節點
size++;
modCount++;
}
複製代碼
與添加節點方法對應的就是刪除節點方法:
/**
* 刪除頭節點
* @return 刪除的節點的值 即 節點的 element
* @throws NoSuchElementException 若是鏈表爲空則拋出異常
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* 刪除尾節點
*
* @return 刪除的節點的值 即 節點的 element
* @throws NoSuchElementException 若是鏈表爲空則拋出異常
*/
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
複製代碼
能夠看出最終調用的方法爲 unlinkFirst
,unlinkLast
方法:
/**
* 移除頭節點
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 頭節點的 element 這裏做爲返回值使用
final E element = f.item;
// 頭節點下個節點
final Node<E> next = f.next;
// 釋放頭節點的 next 指針,和 element 下次 gc 的時候回收這個內部類
f.item = null;
f.next = null; // help GC
// 將 first 索引指向新的節點
first = next;
// 若是 next 節點爲空,即鏈表只有一個節點的時候,last 指向 null
if (next == null)
last = null;
else
next.prev = null; //不然 next 的 prev 指針指向 null
size--;//改變鏈表長度
modCount++;//修改操做數
return element;//返回刪除節點的值
}
/**
* 移除未節點
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
//未節點的前一個節點,
final Node<E> prev = l.prev;
//釋放未節點的內容
l.item = null;
l.prev = null; // help GC
//將 last 索引指向新的未節點
last = prev;
// 鏈表只有一個節點的時候,first 指向 null
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
複製代碼
上邊咱們說過在指定位置添加的節點時候的是4個步驟,移除頭尾節點是兩個特殊的節點,可是整體來講仍是同樣的。下面看到 unlink(node(index))
就是這樣的:
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
//保存 index 節點的先後兩個節點
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 若是節點爲頭節點,則作 unlinkFirst 相同操做
if (prev == null) {
first = next;
} else {//不然將上一個節點的 next 指針指向下個節點
prev.next = next;
// 釋放 index 位置 prev 指針
x.prev = null;
}
// 若是節點爲尾節點,則將 last 索引指向上個節點
if (next == null) {
last = prev;
} else {//不然下個節點 prev 指針指向上個節點
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
複製代碼
看完 unlink
操做結合以前說的 node(index)
,下邊兩種刪除節點的操做,就很好理解了
/**
* 刪除指定索引位置的節點
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
*刪除從頭節點其第一個與 o 相同的節點
*/
public boolean remove(Object o) {
// 區別對待 null 元素,比較元素時候使用 == 而不是 equals
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
複製代碼
看完單個刪除節點的方法 LinkedList 實現了 List 接口的 clear 操做,用於刪除鏈表全部的節點:
/**
* Removes all of the elements from this list.
* The list will be empty after this call returns.
*/
public void clear() {
// 依次清除節點,幫助釋放內存空間
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
複製代碼
LinkedList 查詢節點的方法,可分爲根據指定的索引查詢,獲取頭節點,獲取未節點三種。值得注意的是,根據索引去獲取節點內容的效率並不高,因此若是查詢操做多餘增刪操做的時候建議用 ArrayList
去替代。
/**
* 根據索引查詢
*
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 返回 first 索引指向的節點的內容
*
* @return the first element in this list
* @throws NoSuchElementException 若是鏈表爲空則拋出異常
*/
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 返回 last 索引指向的節點的內容
*
* @return the last element in this list
* @throws NoSuchElementException 若是鏈表爲空則拋出異常
*/
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
複製代碼
修改節點也分爲修改指定索引的節點內容和修改頭節點內容,未節點內容的方法? 哈哈,理所因當了,其實LinkedList
只提供了 set(int index, E element)
一個方法。
public E set(int index, E element) {
// 判斷角標是否越界
checkElementIndex(index);
// 採用 node 方法查找對應索引的節點
Node<E> x = node(index);
//保存節點原有的內容值
E oldVal = x.item;
// 設置新值
x.item = element;
// 返回舊的值
return oldVal;
}
複製代碼
上邊的咱們知道 LinkedList
提供根據角標查詢節點的方法,LinkedList
還提供了一系列判斷元素在鏈表中的位置的方法。
/*
* 返回參數元素在鏈表的節點索引,若是有重複元素,那麼返回值爲從**頭節點**起的第一相同的元素節點索引,
* 若是沒有值爲該元素的節點,則返回 -1;
*
* @param o element to search for
* @return
*/
public int indexOf(Object o) {
int index = 0;
// 區別對待 null 元素,用 == 判斷,非空元素用 equels 方法判斷
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++;
}
}
return -1;
}
/**
**返回參數元素在鏈表的節點索引,若是有重複元素,那麼返回值爲從**尾節點起**的第一相同的元素節點索引,
* 若是沒有值爲該元素的節點,則返回 -1;
*
* @param o element to search for
* @return the index of the last occurrence of the specified element in
* this list, or -1 if this list does not contain the element
*/
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;
}
複製代碼
兩個方法分別返回從頭節點起第一個與參數元素相同的節點索引,和從尾節點起第一個與參數元素相同的節點索引。
除了上述兩個方法咱們開能夠調用 contains(Object o)
來判斷鏈表中是否有該元素存在。調用 indexOf 從頭結點開始查詢元素位置遍歷完成後若 返回值 !=-1 則表示存在,反之不存在
public boolean contains(Object o) {
return indexOf(o) != -1;
}
複製代碼
分析完 LinkedList 做爲 List 集合的增刪改查操做,咱們看下 LinkedList 是如何實現 Deque
接口的方法的:
咱們先來認識一下 Java 中的 雙端隊列,咱們都知道 Queue
是一個隊列,遵循 FIFO 準則,咱們也知道 Stack
是一個棧結構,遵循 FILO 準則。 而Deque
這個雙端隊列就厲害了,它既能夠實現棧的操做,也能夠實現隊列的操做,換句話說,實現了這個接口的類,既能夠做爲棧使用也能夠做爲隊列使用。
咱們來看下 Queue 給咱們提供了的方法:
頭部 | 頭部 | 尾部 | 尾部 | |
---|---|---|---|---|
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | remveLast() | pollLast |
獲取 | getFirst() | peekFirst() | getLast() | peekLast |
因爲 Deque
接口繼承 Queue
接口,當 Deque
當作隊列使用時(FIFO),只須要在頭部刪除,尾部添加便可。咱們如今複習下 Queue
中的方法及區別:
Queue
的 offer
和 add
都是在隊列中插入一個元素,具體區別在於,對於一些 Queue 的實現的隊列是有大小限制的,所以若是想在一個滿的隊列中加入一個新項,多出的項就會被拒絕。此時調用 add()
方法會拋出異常,而 offer()
只是返回的 false。
remove()
和 poll()
方法都是從隊列中刪除第一個元素。remove()也將拋出異常,而 poll()
則會返回 null
element()
和 peek()
用於在隊列的頭部查詢元素。在隊列爲空時, element()
拋出一個異常,而 peek()
返回 null
。
上述方法的區別對於 Queue 對應的實現類的對應方法,是一種規定,本身在實現 Queue 隊列的時候也要遵循此規則。
咱們經過下邊的表格來對照下雙端隊列是如何實現隊列操做的,值得注意的是 Deque
實現了 Queue
,因此 Queue
全部的方法 Deque
都有,下面比較的是Deque
區別 Queue
的方法:
Queue | Deque |
---|---|
add(e) | addLast() |
offer(e) | offerLast() |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
由上表咱們能夠看到 Deque
對應的 Queue
的方法,那麼對於他們的實現類 LinkedList
是怎麼實現的呢?
咱們對比下對應的實現方法:
// queue 的添加方法實現,
public boolean add(E e) {
linkLast(e);
return true;
}
// Deque 的添加方法實現,
public void addLast(E e) {
linkLast(e);
}
// queue 的添加方法實現,
public boolean offer(E e) {
return add(e);
}
// Deque 的添加方法實現,
public boolean offerLast(E e) {
addLast(e);
return true;
}
複製代碼
上面說起到 Queue
的 offer
和 add
的區別針對容量有限制的實現,很明顯 LinkedList
的大小並無限制,因此在 LinkedList
中他們的實現並無實質性不一樣。
// Queue 刪除元素的實現 removeFirst 會拋出 NoSuchElement 異常
public E remove() {
return removeFirst();
}
// Deque 的刪除方法實現
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// Queue 刪除元素的實現 不會拋出異常 若是鏈表爲空則返回 null
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// Deque 刪除元素的實現 不會拋出異常 若是鏈表爲空則返回 null
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
複製代碼
// Queue 獲取隊列頭部的實現 隊列爲空的時候回拋出異常
public E element() {
return getFirst();
}
// Deque 獲取隊列頭部的實現 隊列爲空的時候回拋出異常
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
// Queue 獲取隊列頭部的實現 隊列爲空的時候返回 null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
// Deque 獲取隊列頭部的實現 隊列爲空的時候返回 null
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
複製代碼
上述咱們分析了,雙端隊列做爲隊列使用的時候的各個方法的區別,也但是看出 LinkedList
對對應方法的實現,遵循了隊列設計原則。
下面咱們來看看下雙端隊列做爲棧 Stack
使用的時候方法對應關係,與 Queue
不一樣,Stack
自己就是實現類,他擁有 FILO 的原則, Stack
的入棧操做經過 push
方法進行,出棧操做經過 pop
方法進行,查詢操做經過 peek
操做進行。 Deque
做爲棧使用的時候,也遵循 FILO 準則,入棧和出棧是經過添加和移除頭節點實現的。
Stack | Deque |
---|---|
push(e) | addFist(e) |
pop() | removeFirst() |
peek() | peekFirst() |
因爲分析隊列的時候已經分析了addFist
和 removeFirst
,peekFirst
操做的方法了,下邊咱們來顯 push 和 pop 的實現:
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
複製代碼
哇,毫無遮掩的直接調用了 addFirst
和 removeFirst
方法。這樣看來沒啥好分析的了。
在 ArrayList
分析的時候,咱們就知道 List
的實現類,有4中遍歷方式:for 循環,高級 for 循環,Iterator
迭代器方法, ListIterator
迭代方法。因爲 ArrayList
源碼分析的時候比較詳細看了源碼,對於不一樣數據結構的 LinkedList
咱們只看下他們的不一樣之處.
LinkedList
沒有單獨 Iterator
實現類,它的 iterator
和 listIterator
方法均返回 ListItr
的一個對象。 LinkedList 做爲雙向鏈表數據結構,過去上個元素和下個元素很方便。
下邊咱們來看下 ListItr
的源碼:
private class ListItr implements ListIterator<E> {
// 上一個遍歷的節點
private Node<E> lastReturned;
// 下一次遍歷返回的節點
private Node<E> next;
// cursor 指針下一次遍歷返回的節點
private int nextIndex;
// 指望的操做數
private int expectedModCount = modCount;
// 根據參數 index 肯定生成的迭代器 cursor 的位置
ListItr(int index) {
// assert isPositionIndex(index);
// 若是 index == size 則 next 爲 null 不然尋找 index 位置的節點
next = (index == size) ? null : node(index);
nextIndex = index;
}
// 判斷指針是否還能夠移動
public boolean hasNext() {
return nextIndex < size;
}
// 返回下一個帶遍歷的元素
public E next() {
// 檢查操做數是否合法
checkForComodification();
// 若是 hasNext 返回 false 拋出異常,因此咱們在調用 next 前應先調用 hasNext 檢查
if (!hasNext())
throw new NoSuchElementException();
// 移動 lastReturned 指針
lastReturned = next;
// 移動 next 指針
next = next.next;
// 移動 nextIndex cursor
nextIndex++;
// 返回移動後 lastReturned
return lastReturned.item;
}
// 當前遊標位置是否還有前一個元素
public boolean hasPrevious() {
return nextIndex > 0;
}
// 當前遊標位置的前一個元素
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// 等同於 lastReturned = next;next = (next == null) ? last : next.prev;
// 發生在 index = size 時
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
// 刪除鏈表當前節點也就是調用 next/previous 返回的這節點,也就 lastReturned
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//調用LinkedList 的刪除節點的方法
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
//上一次所操做的 節點置位空
lastReturned = null;
expectedModCount++;
}
// 設置當前遍歷的節點的值
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
// 在 next 節點位置插入及節點
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
//簡單哈操做數是否合法
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
複製代碼
參考下圖理解下,LinkedList 的迭代器的三個變量。
本文從 LinkedList
的源碼出發,分析了LinkedList 集合常見的操做,以及它做爲隊列或者棧的時候增刪改查方法。是繼上一篇 ArrayList
源碼分析後的第二篇集合框架源碼分析。
你見過凌晨3點北京的太陽嗎?沒有!三點太陽還沒升起呢~