【Java原始碼】基於連結串列實現的LinkedList
眾所周知,LinkedList是基於連結串列實現的。
目錄
基本屬性
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
基本屬性中給出了長度,頭結點和尾結點,都是transient,即 不可序列化。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; } }
由定義可以看出,每個結點都包含指向其前一個和後一個結點的兩個結點,還包含一個“資料”,型別為泛型 E
好吧,明擺著雙向連結串列嘛。
構造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
兩個構造方法,無參構造和根據已有Collection構造LinkedList;
根據已有Collection構造LinkedList先是無參構造,再將c全部add進去,既然這裡用到了add,那咱就先看LinkedList元素的增加方法
增加元素(插入元素)
public boolean add(E e) {
linkLast(e);
return true;
}
// 頭插
public void addFirst(E e) {
linkFirst(e);
}
// 尾插
public void addLast(E e) {
linkLast(e);
}
boolean add(E e)
返回值為boolean型別,增加成功返回true;add方法是在其最後加入待加元素。
void linkLast(E e) 在list最後插入元素
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++;
}
一句話:原list為空,則引數做新頭;若不為空,則引數加到末節點後。具體如下:
這個方法先拿出原末節點l,根據末節點l和引數建立新節點,更新新末節點last
判斷原末節點是否為空,若是,則證明原先list無元素,新節點為list首節點;若非空,將新節點加到原末節點l後。更新size和modCount,根據上次ArrayList的經驗,modCound應該是在迭代器中做判斷的。
void linkFirst(E e) 在list頭插入元素
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++;
}
思想如上說過的尾插法,因為LinkList基於雙向連結串列實現,所以插入操作只需改變結點兩個指標指向就行,這點比ArrayList的增加插入要簡便的多!
void add(int index, E element)
在下標位置處插入元素
引數:一個下標,一個元素。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
首先檢查下標合法性checkPositionIndex(index)
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
當然進行判斷,判斷該下標是不是最後。來決定如何插入。
說說 linkBefore(element, node(index)
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++;
}
傳入引數為一個元素,和一個結點(通過下標得到的)、
取出引數結點的前驅結點,以新元素創造一個新節點,連線至前驅結點後引數結點前
再更新各結點指向關係完成操作!
boolean addAll(Collection<? extends E> c)
返回值boolean,引數為一個集合
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
呼叫
boolean addAll(int index, Collection<? extends E> c)
另外傳入一個index下標。即插入到該下標位置
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
// 將引數轉化為陣列,若次陣列長度為0.則直接返回false
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// 準備兩個結點
// 可以理解為:這兩個結點就是引數c的頭和尾
// 頭list連線待插元素,尾list連線原list下標及之後元素
// 判斷插入的位置是否在原list最後(判斷尾的位置)
// 因為在尾部直接加和在中間插入一段的操作完全不同
// 這裡用到了node(index)方法。取出下標結點,尾list,這個方法我一會說。
// 頭list當然就是尾之前的一個嘍
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 思路很簡單了:將引數轉化的陣列,依次插入到頭list後;
// 再將尾list接到頭list之後 (當然這裡肯定要分null和非null情況)
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;
}
首先檢查下標合法性checkPositionIndex(index)。上面說過這個方法
程式碼如上,因為比較長,以註釋形式寫出了。
這是剛才說到的取下標結點的方法 Node<E> node(int index)
Node<E> node(int index) {
// assert isElementIndex(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.刪除頭或尾元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
E removeFirst() 和 E removeLast() 兩個方法,字面意思,移除並返回頭或尾的元素。返回為泛型E,呼叫E unlinkLast(Node<E> node)方法和E unlinkFirst(Node<E> node)方法
E unlinkFirst(Node<E> node) 斷開傳入頭結點的連線並返回該結點元素,移除該結點
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;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
根據傳入結點的得到其元素,以後返回的就是他。
得到傳入結點的下一個元素,目的是更新頭結點。舊頭結點的各“指標”和 元素 也要更新為null。為什麼?方便GC(垃圾回收裝置)回收,釋放記憶體。
同時新頭結點的資訊(前驅結點)也要更新為null;接著就是更新List的各屬性。
E unlinkLast(Node<E> node) 斷開傳入尾結點的連線並返回該結點元素,移除該結點
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 = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
看,思路同unlinkFirst(Node<E> node),主要操作:更新結點資訊!置空!方便GC回收!
2.刪除List元素(根據下標)
E remove(int index)
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
引數:下標
檢查下標合法,不多說了,要刪除就要先找到這個下標結點,Node<E> node(int index)取結點啊,這個方法上面說過!
(回顧一下:根據下標和List的size比較,決定從頭還是尾開始遍歷!)
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
主要在於E unlink(Node<E> 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;
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;
}
思路:得到結點元素,更新傳入結點的指向關係,這個很好理解。此方法兼具刪除頭尾結點的功能,注意判斷!
3.刪除List元素(根據元素(物件object))
boolean remove(Object o)
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;
}
原始碼給出的方法很好理解:判斷引數是否為null,再分別從頭至尾遍歷,找出包含此元素的結點,用unlink(Node<E> x)刪除之。
我們發現原始碼中還有一個boolean removeLastOccurrence(Object o)
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;
}
同上,不過是逆向遍歷,另!刪除逆向找到的第一個元素立刻就返回了,達到刪除最後一個引數元素的目的!
其他方法
clean() 清空List
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++;
}
從頭開始遍歷List,置空,以方便GC回收
E get(int index) 根據下標獲取下標結點元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
無容置疑,一旦涉及下標,就要判斷下標合法性,然後通過node(int index)得到下標結點,進而得到結點內的元素
E set(int index,E element) 根據下標獲取並設定下標結點元素
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
當然,檢查下標,通過node(int index)方法獲得下標結點,進而得到其更改前元素,設定新元素,返回舊元素
另外,原始碼中還有,peek,push,pop等一系列簡單的方法,就是基於上面連結串列的增加,和刪除操作完成的。
迭代器
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;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
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++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
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();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代器採用了以內部類方式實現了Iterator介面
lastReturned; next; nextIndex;
用以上三個來更新位置 expectedModCount = modCount;
前面好多方法都有modCount++的原因了 是一個標識,用來檢查在使用迭代器的同時,LInkedList有沒有被外面修改,如果在使用迭代器的時候對List進行修改,就會丟擲異常
hasNext() 只需檢查遊標有沒有到size,返回true或者false
如果仍有元素可以迭代或有多個元素,則返回 true。
next() 返回迭代的下一個元素。
remove() 從迭代器指向的 collection 中移除迭代器返回的最後一個元素,並將遊標移動到此處,證明此處是有元素的,這也就 同時解釋了沒有用 == null判斷了
checkForComodification() 用來檢查在使用迭代器的同時,List有沒有被外面修改的方法
迭代器的實現,不管實在ArrayList還是LinkedList中實現的思路都是一樣的。我認為,其核心還是在於為保證安全對modCount的判斷。
總結:
LinkedList是基於雙向連結串列實現的
LinkedList也不是執行緒安全的
在增加,刪除,查詢,設定指定下標元素的時候,都要先判斷以決定從頭or從尾開始遍歷
相比於ArrayList,LinkedList在增刪方面更為出色,ArrayList則需要大量元素的移動,甚至list的複製和擴容,但在取方面,推薦ArrayList。這就是為什麼LinkList中有堆疊的pop和push方法,而ArrayList沒有了。可見,在Java原始碼中,堆疊和佇列也是用LinkedList實現的。