LinkedList
當比較LinkedList和ArrayList的區別時我們也許知道前者底層實現是鏈表,後者底層實現是數組,對於ArrayList在【此文】中詳細介紹了,但是對於LinkedList的理解僅僅侷限在鏈表而已,現在一起來看看它的底層實現吧!
類圖
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
我們發現LinkedList實現了Deque
接口,它提供了對隊列操作的相關方法。所以我們經常使用LinkedList去完成隊列相關操作。
類前註釋
雙向鏈表實現了List
和Dqeue
接口,它實現了所有的可選列表操作,它允許所有的元素類型,包括null
。
注意:LinkedList是非線程安全的。如果多個線程同時訪問鏈接列表,並且至少有一個線程在結構上修改列表,則必須在外部進行同步。 (結構修改是添加或刪除一個或多個元素的任何操作;僅設置元素的值不是結構修改。)這通常通過在自然封裝列表的對象上進行同步來實現。 如果沒有這樣的對象存在,列表應該使用Collections.synchronizedList
方法“包裝”。 這最好在創建時完成,以防止意外的不同步訪問列表。
LinkedList的iterator和listIterator方法返回的迭代器是fail-fast
的:如果列表在迭代器創建之後的任何時間被結構化地修改,除了通過迭代器自己的remove或add方法之外,迭代器將會拋出ConcurrentModificationException
。 因此,面對併發修改,迭代器將快速而乾淨地失敗,而不是在未來未確定的時間冒着任意的非確定性行爲。
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;
}
}
看到next
和prev
就知道LinkedList不是單鏈表,是雙向鏈表。
成員變量
transient int size = 0; //記錄LinkedList的大小
transient Node<E> first; //表示LinkedList的頭節點。
transient Node<E> last; //表示LinkedList的尾節點。
構造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList提供兩個構造方法,其中需要注意的是第二個,它接受一個Collection來完成初始化。關鍵在於addAll
方法。
public boolean addAll(Collection<? extends E> c) {
//addAll方法可以在指定索引的節點後面插入Collection中的元素,初始化時size爲0,即從頭開始插入
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//檢查index是否合法,判斷規則:index >= 0 && index <= size;
//不合法拋出IndexOutOfBoundsException
checkPositionIndex(index);
//將Collection轉爲數組
Object[] a = c.toArray();
int numNew = a.length;
//插入的集合爲空,直接返回false
if (numNew == 0)
return false;
//pred:index索引節點的前驅
//succ:index索引表示的節點
Node<E> pred, succ;
if (index == size) {
//在最末端插入節點
succ = null;
pred = last;
} else {
//添加位置在鏈表中
//node()方法用於尋找index索引表示的節點
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)
//pred爲null說明鏈表中無節點
first = newNode;
else
pred.next = newNode;//設置前驅的後繼
//③、更新前驅
pred = newNode;
}
//④、鏈接成完整鏈表
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//更新鏈表的size
size += numNew;
modCount++;
return true;
}
linkFirst
私有方法,插入一個元素,並將它置爲頭節點。
private void linkFirst(E e) {
//備份頭節點
final Node<E> f = first;
//創建新節點
final Node<E> newNode = new Node<>(null, e, f);
//將新節點設置爲頭節點
first = newNode;
if (f == null)
//f爲空說明鏈表爲空,插入的這個節點也是尾節點
last = newNode;
else
//設置原來頭節點的前驅爲新節點
f.prev = newNode;
size++;
modCount++;
}
linkLast
插入一個元素,並將它置爲尾節點。
void linkLast(E e) {
//備份尾節點
final Node<E> l = last;
//創建新節點
final Node<E> newNode = new Node<>(l, e, null);
//將新節點設置爲尾節點
last = newNode;
if (l == null)
//f爲空說明鏈表爲空,插入的這個節點也是頭節點
first = newNode;
else
//設置原來尾節點的後繼爲新節點
l.next = newNode;
size++;
modCount++;
}
getFirst
重寫來自Deque
的方法,返回頭節點,如果頭節點爲null拋出異常。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
getLast
重寫來自Deque
的方法,返回尾節點,如果尾節點爲null拋出異常。
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
removeFirst
重寫來自Deque
的方法,刪除頭節點並返回舊值,如果頭節點爲null拋出異常。
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// 斷言 f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;//將數據域和後繼設爲null,GC幫助銷燬
f.next = null; // help GC
//重新設置頭節點
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
removeLast
重寫來自Deque
的方法,刪除尾節點並返回舊值,如果尾節點爲null拋出異常。
remove
刪除等於指定元素的第一次出現的節點。刪除成功返回true
,失敗返回false
。
addFirst
重寫來自Deque
的方法,從頭部添加節點。藉助linkFirst
方法完成此方法。
addLast
重寫來自Deque
的方法,從尾部添加節點。藉助linkLast
方法完成此方法。
add
往鏈表中添加節點,從尾部插入。offer
方法調用此方法實現尾插功能。
clear
清空整個鏈表。其實就是遍歷整個鏈表,將數據域,前驅,後繼置爲null。
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++;
}
get
獲取指定索引的元素值。
public E get(int index) {
//判斷索引的合法性
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
//如果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;
}
}
set
爲指定索引的節點設置值,返回舊值。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
indexOf
返回此列表中指定元素的第一次出現的索引,如果此列表不包含元素,則返回-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;
}
與indexOf
相似的還有lastIndexOf
這一方法,它返回此列表中指定元素的最後一次出現的索引,如果此列表不包含元素,則返回-1。
contains
查找鏈表中是否有知道的元素值,返回對應的索引(第一次出現的位置),使用indexOf
實現此方法。
public boolean contains(Object o) {
return indexOf(o) != -1;
}
peek
返回頭節點的值,如果頭節點爲null則返回null。
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
與之類似的方法還有element
,但是不同的是如果鏈表爲空則拋出NoSuchElementException
。
peekLast
返回尾節點的值,如果頭節點爲null則返回null。
poll
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
出隊操作,返回隊首元素值。
push和pop
push
方法從頭部插入元素,pop
從頭部刪除元素。常使用這兩個方法實現棧操作。
迭代器
LinkedList爲我們提供了兩種迭代器,分別爲ListIterator
和DescendingIterator
。使用ListIterator
迭代器我們可以完成遍歷,刪除和修改節點的操作,同時這也是官方推薦我們遍歷鏈表的方式,而不是使用fori
遍歷鏈表。使用ListIterator
我們不僅可以正向遍歷鏈表也可以反向遍歷。
descendingIterator
方法來自Deque
接口,使用DescendingIterator
我們只能從後向前遍歷,並且只提供刪除操作,無法添加和修改節點。
clone
同ArrayList
一樣,LinkedList
的clone方法實現的是淺拷貝。
toArray
此方法將鏈表中的值放在數組中,並將數組返回。
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;
}
使用此方法不能講Object[]強轉爲其他類型,否則拋ClassCastException
異常。如果我們需要實現將鏈表值放在指定類型數組中使用toArray(T[] a)
方法。不過這裏有個小坑哦!
public <T> T[] toArray(T[] a) {
//如果你傳入的數組a不夠大會新建一個數組對象賦給a,
//後序會將值賦值給新創建的數組對象,那麼原來的那個數組將沒有值,全爲null
//所以我們還是統一使用返回的數組就不會錯了
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;
}
文章同步【個人站】