Java集合框架——LinkedList
本文基於JDK1.8,程式碼中方法頭的註釋都是對照原始碼翻譯過來的 自頂向下閱讀
類頭
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
...
}
LinkedList繼承了AbstractSequentialList抽象類,是一個雙向連結串列,可以被當作堆疊、佇列或雙端佇列進行操作。在AbstractSequentialList封裝好了一些方法和抽象方法,當繼承此抽象類時,只需要重寫其中的抽象方法,而不需要重寫全部方法 實現List介面(提供List介面中所有方法的實現) 實現了Deque介面,實現了Deque所有的可選的操作 實現Cloneable介面,支援克隆,可以呼叫clone()進行拷貝 實現java.io.Serializable介面,支援序列化
核心
LinkedList內部定義了一個Node類,Node是雙向的,LinkedList底層都是通過操作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 item:值域,節點的值 Node next:next域,當前節點的後一個節點的引用(可以理解為指向當前節點的後一個節點的指標) Node prev:prev域,當前節點的前一個節點的引用(可以理解為指向當前節點的前一個節點的指標)
成員變數
//記錄LinkedList的大小
transient int size = 0;
//指向頭節點
transient Node<E> first;
//指向尾結點
transient Node<E> last;
使用transient關鍵字,避免序列化
構造方法
/**
* 預設構造器,建立一個空的列表
*/
public LinkedList() {
}
/**
* 按照集合的迭代器返回的順序,構造包含指定集合的元素的列表
*
* @param c 將其元素放入列表中的集合
* @throws NullPointerException 如果指定集合為空丟擲異常
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
首先呼叫一下空的構造器建立一個連結串列,然後在此構造器中呼叫了addAll(Collection<? extends E> c)方法,後面插入會講到
工具方法
這些方法都是private私有或protected的,不提供給使用者使用,只用作LinkedList公有方法功能的實現 下面這些方法都是頭節點、尾結點成對的,可以對照看,只要弄懂一個,其他的理解很快
1. 把元素作為連結串列的第一個元素
注意:只有在連結串列首部和尾部插入才有頭節點和尾結點的變化,如果在連結串列中插入則沒有
/**
* 連結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++;
}
由於將指定元素作為頭節點,所以需要將原來的頭節點儲存下來,將頭節點賦給一個新節點f,注意這裡是等號直接賦值,賦的是地址,f的改變會直接作用到頭節點first,頭節點也會改變,在LinkedList中有很多這種賦值操作,一定要搞清楚,不然就會有f的改變為什麼會改變整個LinkedList的結構等疑惑 承接上文,再建立一個新節點newNode,將其值域設為引數e,next域指向f,然後將自身設為頭節點 進行判斷,如果f為空,說明頭節點為空,整個連結串列為空,已經將頭節點設為newNode,只需要將新節點設為尾結點即可;若不為空,此時newNode已經是頭節點,且next(下一個節點地址)指向f(正數第二個節點),但LinkList是雙向連結串列,所以下一個節點的prev(前一個節點地址)域需要指向頭節點 然後連結串列大小加1,修改次數加1
2. 把元素作為連結串列的最後一個元素
/**
* 連結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++;
}
和linkFirst(E e)方法原理類似 先儲存尾結點,在構造一個新的節點,prev域指向尾結點,再將新節點newNode作為尾結點 再進行判斷,l為空,空連結串列,頭節點first也指向新節點;不為空,l節點(也就是倒數第二個節點)next域指向尾結點,將連結串列連結起來即可
3. 刪除連結串列第一個節點(節點不為空)
/**
* 解開非空第一節點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;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
定義一個element儲存f節點值域的值,定義一個新節點next指向f節點的下一節點,將f的item域和next域設為空,由於刪除了f,f的下一節點就變為了頭節點,所以first = next
再進行判斷,若next為空,空連結串列,尾結點last設為null;若不為空,頭節點prev域設為空即可(頭節點沒有前一個節點)
最後返回刪除的節點的值域element
總結:f節點item、next域為空,頭節點prev域為空
4. 刪除連結串列最後一個節點(節點不為空)
/**
* 解開非空最後節點l
*/
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;
}
定義element儲存l節點值域的值,定義一個新節點prev指向l節點的前一個節點,將l的item域和prev域設為空,由於刪除了最後一個節點l,l的前一個節點就變為尾結點,所以last = prev
再判斷,空連結串列,頭節點為空;不為空,尾結點的next域設為空(尾結點沒有下一個節點)
最後返回刪除的節點的值域element
總結:l節點item、prev為空,尾節點next域為空
5. 在非空節點succ之前插入元素e
/**
* 在非空節點succ之前插入元素e
*/
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++;
}
先定義一個新節點pred將succ的前一個節點儲存下來,在succ節點之前插入,插入後,待插入元素e節點位於succ節點和succ前一個節點pred之間,所以構造一個待插入新節點newNode,前一個節點為pred,下一個節點為succ
此時只是newNode節點指向前一個節點pred和下一個節點succ,但LinkedList為雙向連結串列,需要下一個節點前驅(prev域)指向newNode,所以succ.prev = newNode
再判斷pred是否為空,為空,新節點設為頭節點;不為空,前一個節點pred的後驅(next域)指向新節點,這樣,就成功插入元素了
6. 刪除指定節點(節點非空)
/**
* 解開非空節點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;
}
前面的刪除方法要麼是刪除第一個節點,要麼是刪除最後一個節點,而此方法是刪除指定結點,實現方式略有區別
首先,和前面類似,刪除某一節點,需要將指定節點的前一節點和下一節點儲存下來,程式碼中將節點的值也儲存下來是作為方法的返回值
然後判斷前一節點prev是否為空,為空,則x為頭節點,刪除之後,x下一節點next為頭節點,所以first = next
;不為空,將prev的後驅(next域)指向x下一節點next,這裡連結第一部分(前一節點指向後一節點)
再判斷後一節點next是否為空,為空,x為尾結點,刪除之後,x前一節點prev為尾結點,所以last = prev
;不為空,將next前驅(prev域)指向x前一節點prev,這裡連結第二部分(後一節點指向前一節點)
最後將item值域設為空,連結串列大小減一,modCount加1,返回刪除節點的值
由於LinkedList是雙向的,需要雙向操作,所以在方法中通過兩個if判斷來分別進行連結串列的連結操作,這裡可以自己畫圖理解
最後返回刪除的節點的值域element
7. 得到指定位置節點
/**
* 返回指定元素索引處的(非空)節點
*/
Node<E> node(int index) {
// assert isElementIndex(index);
/*
判斷index位於連結串列的前半部分還是後半部分,從而判斷從哪個方向遍歷
用迴圈方式遍歷到index那個位置,得到該位置的元素並返回
LinkedList獲取元素採用的是遍歷連結串列的方式,雖然最多隻會迴圈列表大小的一半,但效能也比較低的
*/
if (index < (size >> 1)) { //size >> 1 等同於size/2
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;
}
}
8. 索引檢查
方法內容簡單明瞭,看註釋很容易理解
/**
* 說明引數是否為現有元素的索引
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 說明引數是否是迭代器或新增操作的有效位置的索引
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
注意isElementIndex()和isPositionIndex()的不同,isPositionIndex()是判斷迭代器或新增操作的位置是否有效,所以多了個等於的判斷,若index=size,則是在連結串列末尾插入 isElementIndex(int index)是連結串列元素索引的檢查,所以連結串列的索引一定是小於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));
}
9. 異常資訊提示
/**
* 構造IndexOutOfBoundsException異常詳細資訊
* 在錯誤處理程式碼的許多可能的重構中,這個“大綱”對伺服器和客戶端VM都是最好的
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
到這裡大部分的工具方法就講完了,接下來的公有方法大多是呼叫上面的方法來實現相關功能的
公有方法
因為有些方法具體分析在上面有提到,接下來已經講過的方法就不一一贅述了,結合註釋很容易明白
1. 插入
1.1 addAll(Collection<? extends E> c):boolean
/**
* 按照指定集合的迭代器返回的順序,將指定集合中的所有元素追加到該列表的末尾。如果操作過程中指定的集合被修改,則此操作的行為未定義(注意,如果指定的集合是這個列表,則它將是非空的。)
*
* @param c 包含要新增到此列表的元素的集合
* @return {@code true} 如果此列表由於呼叫而更改
* @throws NullPointerException 如果指定集合為空
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* 將指定集合中的所有元素插入到該列表中,從指定位置開始。將當前在該位置的元素(如果有的話)和任何後續元素向右移動(增加它們的索引)。新元素將以指定集合的迭代器返回的順序出現在列表中。
*
* @param index 從指定集合插入第一個元素的索引
* @param c 包含要新增到此列表的元素的集合
* @return {@code true} 如果此列表由於呼叫而更改
* @throws IndexOutOfBoundsException
* @throws NullPointerException 如果指定集合為空
*/
public boolean addAll(int index, Collection<? extends E> c) {
//先進行邊界檢查
checkPositionIndex(index);
//將集合c轉化為陣列,判斷陣列長度是否符合要求
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//定義了兩個節點,pred(待新增節點的前一個節點)、succ(待新增位置index的節點)
Node<E> pred, succ;
//構造器呼叫進入if程式碼塊中
//if判斷,如果index == size,說明將元素插入到連結串列末尾(構造器如此,連結串列為空),succ沒有意義,設為空,待插入節點pred設為尾結點;否則插入位置位於連結串列中,通過方法E node(int index)得到索引位置的節點,使pred指向succ的前一個節點
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//通過for迴圈遍歷陣列,每次遍歷都新建一個節點,節點中儲存陣列元素的值,使該節點的前驅(prev域)指向pred節點,進行單向連結,再進行if判斷,如果pred為空,則為空連結串列,頭節點指向新節點;若不為空,pred節點後驅(next域)指向新節點,對應new Node<>(pred, e, 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;
}
//再進行succ判斷,經過前面的if判斷,succ只有為null和node(index)兩種情況。為空,尾結點last指向pred;不為空,pred後驅(next域)指向succ,succ的前驅指向pred,就將集合元素成功插入指定位置了
//注意:只有在連結串列首部和尾部元素的改變才有頭節點和尾結點的變化,如果在連結串列中插入則沒有
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//最後連結串列大小加1,modCount加1,返回修改結果true
size += numNew;
modCount++;
return true;
}
在構造器中就呼叫了addAll()方法 可以看到,addAll(int index, Collection<? extends E> c)方法才是構造器的底層實現 方法內容有點多,將分析拆分開來寫在方法註釋中
1.2 add():boolean
/**
* 將指定的元素追加到列表的末尾
*
* <p>該方法相當於addLast
*
* @param e 要追加到這個列表的元素
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
2. 刪除指定元素(且只刪除第一次出現的指定的元素),如果指定的元素在集合中不存在,則返回false,否則返回true)
/**
* 如果存在,則從該列表中移除指定元素的第一次出現。如果該列表不包含元素,則其不變。更正式地,移除具有最低索引的元素
*
* 如果這個列表包含指定的元素(或者等效地,如果這個列表由於呼叫而改變),則返回true
*
* @param o 要從該列表中移除的元素,如果存在
* @return {@code true} 如果此列表包含指定元素
*/
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;
}
3. 得到第一個、最後一個元素
/**
* 返回此列表中的第一個元素
*
* @return 列表中的第一個元素
* @throws NoSuchElementException 如果這個列表是空的丟擲異常
*/
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 返回此列表中的最後一個元素
*
* @return 此列表中的最後一個元素
* @throws NoSuchElementException 如果這個列表是空的丟擲異常
*/
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
4. 刪除第一個、最後一個元素並返回
/**
* 從這個列表中移除並返回第一個元素
*
* @return 列表中的第一個元素
* @throws NoSuchElementException 如果這個列表是空的丟擲異常
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* 從這個列表中移除並返回最後一個元素
*
* @return 列表中的最後一個元素
* @throws NoSuchElementException 如果這個列表是空的丟擲異常
*/
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
5. 在開頭、末尾插入元素
/**
* 在這個列表的開頭插入指定的元素
*
* @param e 要新增的元素
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* 將指定的元素追加到列表的末尾
*
* <p>這個方法相當於add
*
* @param e 要新增的元素
*/
public void addLast(E e) {
linkLast(e);
}
6. 清空連結串列
/**
* 從該列表中移除所有元素
* 此呼叫返回後,列表將為空
*/
public void clear() {
// 清除節點之間的所有連結是"不必要的",但是:
// - 如果丟棄的節點駐留一代以上,則有助於生成GC
// - 即使有一個可達迭代器,也可以釋放記憶體
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,把每個節點都置空。最後要把頭節點和尾節點設定為空,siz也設定為空,但modCount仍自增
7. 判斷是否包含某一元素
/**
* 如果這個列表包含指定的元素,則返回true
*
* @param o 元素在列表中的存在將被測試
*/
public boolean contains(Object o) {
return indexOf(o) != -1;
}
/**
* 返回此列表中指定元素的第一次出現的索引,或如果該列表不包含元素,則為-1
*
* @param o 要搜尋的元素
* @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;
}
contains(Object o)底層呼叫indexOf(Object o)方法 在indexOf(Object o)方法中,先判斷obejct是否為空,分為兩種情況: 然後在每種情況下,從頭節點開始遍歷LinkedList,判斷是否有與object相等的元素,如果有,則返回對應的位置index,如果找不到,則返回-1
8. 獲取對應索引節點的值
/**
* 返回該列表中指定位置的元素
*
* @param index 返回元素的索引
* @return 列表中指定位置的元素
* @throws IndexOutOfBoundsException
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
9. 轉化為陣列
9.1 轉化為陣列(不含引數)
/**
* 返回包含該列表中所有元素的陣列(從第一個元素到最後一個元素)
*
* <p>返回的陣列將是“安全的”,因為該列表沒有維護它(換句話說,這個方法必須分配一個新的陣列)。因此,呼叫方可以修改返回的陣列
*
* <p>該方法作為基於陣列和基於集合的橋樑
*
* @return 以適當順序包含該列表中的所有元素的陣列
*/
public Object[] toArray() {
Object[] re