探索LinkedList底層實現
前言
老樣子,還是先看註釋!本來以為能收穫點什麼乾貨,結果註釋與ArrayList
的註釋基本相同,有點尷尬...LinkedList的原始碼是基於JDK1.8
。
開幹
先上LinkedList類的註釋,在深入到類中詳細說明屬性與方法。
閱讀註釋
它的意思是:通過索引來操作LinkedList的話,每次都需要從頭或從尾開始一個一個遍歷,直到遇到指定的索引。
也是非執行緒安全,所以在多執行緒環境下要在外部控制同步防止資料紊亂。
不多說了。
有一個點要注意下,獲取迭代器後,不能在直接通過物件呼叫方法去修改結構,只能通過迭代器去呼叫,否則將會丟擲異常。
資料結構
接下來將詳細說明LinkedList類的屬性與方法,在這之前,先通過一張圖簡單瞭解下它的資料結構。
很簡單的一張圖,每一個節點都儲存了當前節點的資訊,同時又關聯了下一個節點與上一個節點。若要獲取指定節點資訊,那麼只能通過這種關聯關係來找到具體節點,這也奠定了它查詢
的速度相對於ArrayList
來說要慢很多,但也是由於有這層關係,新增/刪除
的速度相對於ArrayList
來說要更快,畢竟它只需要更改當前節點關聯的上下節點關係即可,而ArrayList需要移動一波元素
。
//支援克隆、序列化、雙端佇列 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { //連結串列的容量大小,意指包含多少個節點 transient int size = 0; //連結串列的第一個節點,也就是首部節點 //連結串列中的每一個節點都儲存著當前節點的資訊、上一個節點的地址、下一個節點的地址 //首部節點的上一個節點指向null transient Node<E> first; //連結串列的最後一個節點,也就是尾部節點 //尾部節點的下一個節點指向null //有了首部節點為什麼還要有個尾部節點呢,個人理解是為了方便遍歷計算 transient Node<E> last; }
LinkedList的底層是通過連結串列實現的,並不需要提供成員屬性來預先開闢記憶體空間以便後續的儲存節點,所以它充分利用了記憶體空間,實現靈活的記憶體管理
,不存在什麼擴容機制。也正因為此,它並未提供有入參initCapacity的建構函式,在實現上看起來比ArrayList更簡單。
建構函式
/** * 空引數建構函式 */ public LinkedList() { } /** * 構造一個包含指定集合的連結串列 * 指定集合中的節點按照迭代器的順序依次新增到連結串列的尾部 * @param c 指定集合 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
感覺上確實比ArrayList簡單多了,不用考慮那麼多,直接new一個物件即可。
方法說明
接下來按照類的宣告順序介紹方法
,有必要的情況下結合例子進行說明。
簡單方法
/**
* 新增指定節點到首部
* 指定節點作為首部節點,它的上一個節點指向null
* 若是空連結串列,當新增指定節點時首部節點與尾部節點指向同一個節點,即指定節點
* 若不是空連結串列,則將原先的首部節點的上一個節點指向指定節點
* @param e 指定節點
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
/**
* 新增指定節點到尾部
* 指定節點作為尾部節點,它的下一個節點指向null
* 若是空連結串列,當新增指定節點時首部節點與尾部節點指向同一個節點,即指定節點
* 若不是空連結串列,則將原先的尾部節點的下一個節點指向指定節點
* @param e 指定節點
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
/**
* 新增指定節點到指定節點succ前
* 在新增節點時,更換對應上下節點的指向地址
* @param e 新增的指定節點
* @param succ 指定節點處
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
/**
* 刪除首部節點
* @param f 首部節點
*/
private E unlinkFirst(Node<E> f) {
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
/**
* 刪除尾部節點
* @param l 尾部節點
*/
private E unlinkLast(Node<E> l) {
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
/**
* 刪除指定節點
* @param x 指定節點
* @return 指定節點的資訊
*/
E unlink(Node<E> x) {
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;
}
/**
* 判斷是否包含指定資訊
* 一個節點一個節點的判斷是否與指定資訊相等
* @param o 指定資訊
* @return 是否包含指定資訊
*/
public boolean contains(Object o) {
return indexOf(o) != -1;
}
/**
* 正向遍歷的方式獲取指定資訊的位置
* 由於是連結串列的資料結構,每個節點只知道自己的資訊與上下兩個節點,所以每次查詢時都只能從頭/尾開始遍歷
* 相當於是一個人一個人的問,直到問到自己想要的人
* 若未找到指定節點則返回-1
* @param 0 指定資訊
* @return 指定資訊的位置或-1
*/
public int indexOf(Object o) {
int index = 0;
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;
}
/**
* 獲取節點個數
*/
public int size() {
return size;
}
/**
* 清空所有節點資訊
*/
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++;
}
/**
* 判斷指定位置是否越界
* @param index 指定位置
* @return 是否越界
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 判斷指定位置是否合法
* @param index 指定位置
* @return 是否合法
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
* 獲取指定位置越界或不合法的資訊
* @param index 指定位置
* @return 資訊
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
/**
* 校驗指定位置是否越界
* @param index 指定位置
*/
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 校驗指定位置是否合法
* @param index 指定位置
*/
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 獲取指定位置的節點
* 先判斷指定位置處於0-mid-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;
}
}
/**
* 反向遍歷的方式獲取指定資訊的位置
* 由於是連結串列的資料結構,每個節點只知道自己的資訊與上下兩個節點,所以每次查詢時都只能從頭/尾開始遍歷
* 相當於是一個人一個人的問,直到問到自己想要的人
* 若未找到指定節點則返回-1
* @param 0 指定資訊
* @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;
}
/**
* 儲存每個節點的資訊及指向上下節點的地址
*/
private static class Node<E> {
//當前節點的資訊
E item;
//當前節點的下一個節點
Node<E> next;
//當前節點的上一個節點
Node<E> prev;
/**
* 初始化引數
* @param prev 當前節點的上一個節點
* @param element 當前節點的資訊
* @param next 當前節點的下一個節點
*/
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
/**
* 呼叫該clone之前,該類要實現Cloneable,不然會丟擲異常
* 淺拷貝與深拷貝,舉個例子吧
* 比如A類中包含基本型別與B類,當呼叫A類clone方法後,兩個A物件肯定是不一致,不然就不叫做拷貝了,不過這不是關鍵
* 若A1物件中的B物件與A2物件中的B物件指向同一個物件,則認為它是淺拷貝,認為B沒有被拷貝新的一份
* 若兩者指向不相等的話,則認為深拷貝,認為B重新拷貝了一份,不過這通常需要我們自定義程式碼,就像下面的方法一樣
* @return 克隆後的新物件
*/
@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
public Object clone() {
LinkedList<E> clone = superClone();
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
/**
* 構造新陣列,存放連結串列中的所有節點資訊
* @return 包含所有節點資訊的新陣列
*/
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
/**
* 將連結串列中的所有節點資訊放入到指定陣列中並返回
*
* 一開始我也好奇為啥要在索引為size上設定個null呢?
* 看了註釋加上自我的理解,若傳入的新陣列是個空陣列的話,那麼除了拷貝列表元素後剩餘的所有空間的值都為null,此時在給索引為size的值設定成null似乎沒有多大
* 意思;另外一種情況是若傳入的新陣列不是個空陣列,那這個設定就有意義了,傳入的新陣列的某些元素會被列表元素覆蓋,同時有個null,剩下的才是自己本身的資料,呈現這樣子一種效果
*
* List<Integer> list = new ArrayList<>();
* list.add(11);
*
* Integer[] str = new Integer[]{1,2,3,4,5,6,7,8,9,10};
* Integer[] s1 = list.toArray(str);
*
* for (Integer s : s1) {
* System.out.println(s + ",");
* }
*
* 輸出結果:11,null,3,4,5,6,7,8,9,10,
* 那麼設定這個null的意義就在於能夠確定列表中元素個數(長度),但有個前提就是呼叫者知道連結串列中的所有節點資訊不存在null才有意義,目前我只有想到這一種情況下有用!
*
* @param a 指定陣列
* @return 填充完連結串列所有節點資訊的指定陣列
*/
public <T> T[] toArray(T[] a) {
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
/**
* 自定義序列化
* 每個節點當中儲存的上下節點的關聯關係對於序列化來說可有可無,只要你按次序儲存好每個節點的資訊,反序列化後依然可以構造出這些關聯關係
* 預設的序列化機制會將非靜態與非瞬時(非transient修飾)寫入流中
* @param s 輸出流
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(size);
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
/**
* 自定義反序列化
* @param s 輸入流
*/
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int size = s.readInt();
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
/**
* 獲取分割迭代器
* 分割迭代器的方法作用都是類似的,不同的地方在於演算法的實現上,所以不在做重複的分析,可參考另外一篇文章的詳細介紹
* 附上文章地址:http://zlia.tech/2019/08/28/explain-arraylist-spliterator-sourcecode
*/
public Spliterator<E> spliterator() {
return new LLSpliterator<E>(this, -1, 0);
}
很多方法都沒有過多的說明,是因為可能涉及到演算法或者只要簡單畫一畫就能理解了!
獲取節點
/**
* 獲取首部節點資訊
* 若是空連結串列,則會丟擲異常
* @return 首部節點資訊
*/
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 獲取尾部節點資訊
* 若是空連結串列,則會丟擲異常
* @return 尾部節點資訊
*/
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
/**
* 獲取指定位置的資訊
* 校驗位置是否越界
* @param index 指定位置
* @return 指定位置的資訊
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 獲取首部節點資訊
* 若是空連結串列,則會返回null
* 該方法比getFirst更友好
* @return 首部節點資訊
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
* 獲取首部節點資訊
* 若是空連結串列,則會丟擲異常
* @return 首部節點資訊
*/
public E element() {
return getFirst();
}
/**
* 獲取首部節點資訊
* 若是空連結串列,則會返回null
* 該方法比getFirst更友好
* @return 首部節點資訊
*/
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
* 獲取尾部節點資訊
* 若是空連結串列,則會返回null
* 該方法比getLast更友好
* @return 首部節點資訊
*/
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
移除節點
/**
* 移除首部節點
* 若是空連結串列,則會丟擲異常
* @return 首部節點的資訊
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* 移除尾部節點
* 若是空連結串列,則會丟擲異常
* @return 尾部節點的資訊
*/
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
/**
* 移除指定資訊
* 由於是連結串列的資料結構,每個節點只知道自己的資訊與上下兩個節點,所以每次查詢時都只能從頭開始遍歷
* 相當於是一個人一個人的問,直到問到自己想要的人
* 若未找到指定節點則返回false
* @param o 指定資訊
* @return 是否移除成功
*/
public boolean remove(Object o) {
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;
}
/**
* 移除指定位置的節點
* @param index 指定位置
* @return 舊節點資訊
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* 移除首部節點,相當於佇列取出元素
* 若是空連結串列,則會返回null
* @return 舊節點資訊
*/
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 移除首部節點
* 若是空連結串列,則會丟擲異常
* @return 首部節點的資訊
*/
public E remove() {
return removeFirst();
}
/**
* 移除首部節點,相當於佇列取出元素
* 若是空連結串列,則會返回null
* @return 舊節點資訊
*/
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 移除尾部節點,相當於佇列取出元素
* 若是空連結串列,則會返回null
* @return 舊節點資訊
*/
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
/**
* 正向遍歷,刪除指定節點資訊
* @param o 指定節點資訊
* @return 是否刪除指定節點資訊成功
*/
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
/**
* 反向遍歷,刪除指定節點資訊
* @param o 指定節點資訊
* @return 是否刪除指定節點資訊成功
*/
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
新增節點
/**
* 新增指定資訊到首部
* @param e 指定資訊
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* 新增指定資訊到尾部
* @param e 指定資訊
*/
public void addLast(E e) {
linkLast(e);
}
/**
* 新增指定資訊到尾部
* @param e 指定節點資訊
* @return 是否新增成功
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 新增指定集合資訊到尾部
* @param c 指定集合資訊
* @return 是否新增成功
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* 在指定位置新增指定集合資訊
* @param index 指定位置
* @param c 指定集合資訊
* @return 是否新增成功
*/
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
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("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
/**
* 新增指定節點資訊到指定位置上
* 在新增過程中只需要修改舊節點的上下節點關聯關係即可,可以說效率提升了很多
* @param index 指定位置
* @param element 指定節點資訊
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* 新增指定節點資訊到尾部
* @param e 指定節點資訊
* @return 是否新增成功
*/
public boolean offer(E e) {
return add(e);
}
/**
* 新增指定節點資訊到首部
* @param e 指定節點資訊
* @return 是否新增成功
*/
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* 新增指定節點資訊到尾部
* @param e 指定節點資訊
* @return 是否新增成功
*/
public boolean offerLast(E e) {
addLast(e);
return true;
}
修改節點
/**
* 修改指定位置的節點資訊
* @param index 指定位置
* @param element 指定節點資訊
* @return 舊節點資訊
*/
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
迭代器
/**
* 獲取包含指定位置開始到結尾之間的節點的正向迭代器
* @param index 指定位置
* @return 包含指定位置到結尾之間的節點的迭代器
*/
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
/**
* 列表迭代器,正向迭代
* 可獲取上一個元素、下一個元素及位置
*/
private class ListItr implements ListIterator<E> {
//指向當前節點
private Node<E> lastReturned;
//指向上/下一個節點,隨著呼叫方法的不同指向便不同
private Node<E> next;
//上/下一個節點的位置
private int nextIndex;
//每次呼叫迭代器中的方法時,都會判斷是否呼叫了LinkedList的外部方法去修改結構列表,如此的操作通常會造成快速失敗
private int expectedModCount = modCount;
/**
* 初始化引數,設定下一個節點及下一個節點的位置
* @param index 當前位置,呼叫迭代器中的方法時該位置對應的節點資訊是第一個獲取到的節點
*/
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
/**
* 判斷是否有下一個節點
* @return 是否有下一個節點
*/
public boolean hasNext() {
return nextIndex < size;
}
/**
* 獲取下一個節點的資訊
* 初始化時的入參index對應的節點作為開始
* 在獲取下一個節點資訊前,會判斷是否造成了快速失敗
* 在獲取完下一個節點後,將其指向賦值給lastReturned,同時修改next指向下一個節點
* @return 下一個節點的資訊
*/
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
/**
* 判斷是否有上一個節點
* @return 是否有上一個節點
*/
public boolean hasPrevious() {
return nextIndex > 0;
}
/**
* 獲取上一個節點的資訊
* 初始化時的入參index對應的節點的上一個節點作為開始
* 在獲取下一個節點資訊前,會判斷是否造成了快速失敗
* @return 上一個節點的資訊
*/
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
/**
* 獲取下一個節點的位置
* @return 下一個節點的位置
*/
public int nextIndex() {
return nextIndex;
}
/**
* 獲取上一個節點的位置
* @return 上一個節點的位置
*/
public int previousIndex() {
return nextIndex - 1;
}
/**
* 移除當前節點,也就是lastReturned指向的節點
*/
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
/**
* 將當前節點的資訊修改成指定資訊
* @param e 指定資訊
*/
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
/**
* 在下一個節點前新增指定節點
* 若下一個節點指向null,也就是說當前節點已經是尾部節點了,在將指定節點新增到尾部
* 若下一個節點指向不為null,則在下一個節點前新增指定節點
* @param e 指定節點資訊
*/
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
/**
* 正向遍歷連結串列的剩餘節點,對每一個節點執行指定的動作
* 對於每個迭代器,除非剩餘節點未遍歷完畢,否則該方法只能執行一次,因為隨著遍歷nextIndex的次數在增加
* @param action 對每一個節點執行指定的動作
*/
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
/**
* 初始化時 modCount 與 expectedModCount 是相等的
* 但如果在遍歷的過程修改陣列結構的話,此時 modCount 會有所變化,導致兩者不相等,故而丟擲異常,也就是我們上面提到的fast-failed異常
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
/**
* 獲取反向迭代器,尾部節點作為開始
* @return 以尾部節點作為開始的反向迭代器
*/
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
/**
* 列表迭代器,反向迭代
* 實際上都是採用ListItr類中的方法,只不過包裝了一層
*/
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
/**
* 從反向的角度來看,判斷是否有下一個節點
* @return 是否有下一個節點
*/
public boolean hasNext() {
return itr.hasPrevious();
}
/**
* 從反向的角度來看,獲取下一個節點資訊
* @return 下一個節點資訊
*/
public E next() {
return itr.previous();
}
/**
* 從反向的角度來看,移除當前節點
*/
public void remove() {
itr.remove();
}
}
- 佇列中的方法簡單說明下:
peek、peekFirst、elements方法都是獲取首部節點
,但若是空連結串列的話,前兩者不會丟擲異常(返回null),最後一個會。
peekLast方法是獲取尾部節點
。
poll、pollFirst、remove、pop方法都是刪除並首部節點
,但若是空連結串列的話,前兩者不會丟擲異常(返回null),後兩者會。
pollLast方法是刪除並獲取尾部節點
。
offer與offerLast方法都是新增指定節點到尾部
,沒啥區別。
offerFirst、push方法是新增指定節點到首部
,兩者的區別在於有無返回值。
總結
-
LinkedList允許存放Null。
-
LinkedList內部通過連結串列實現,屬於非執行緒安全。
-
LinkedList充分利用了記憶體空間,不存在擴容機制。
-
ArrayList具有
iterator
與listIterator
,雖然LinkedList也有這兩個方法,但實際上這兩個方法的內部實現都是呼叫的listIterator
。 -
在遍歷過程中不允許修改結構,否則會丟擲錯誤。
-
LinkedList實現了佇列。
重點關注
底層是通過連結串列實現,有序可重複
充分利用記憶體空間