List介面下的集合原始碼分析——LinkedList
原始碼版本JDK1.8
今天帶來的是List的另一種實現——LinkedList,這是一種基於雙向連結串列實現的列表。接下來讓我們通過原始碼來分析一下它吧。
關於原始碼中的一些小改動
在JDK1.6及之前,LinkedList底層是一個雙向迴圈連結串列,容器中的元素都是靜態內部類Entry的物件,列表中必有一個空頭結點;
在JDK1.7及之後,LinkedList底層是一個雙向非迴圈連結串列,容器中的元素都是靜態內部類Node的物件。
基於這些小差別,筆者分享下自己的見解:
- 使用非迴圈連結串列後,可以少一個空的頭結點,在頭尾加入元素時可以少一些引用操作(對於迴圈連結串列來說,由於首尾相連,還是需要處理兩頭的前驅和後繼引用。而非迴圈連結串列只需要處理一邊first.previous/last.next,所以理論上非迴圈連結串列更高效。恰恰在兩頭(鏈頭/鏈尾) 操作是最普遍的)
- 對於Entry改變成Node,本質上是沒有差別的。可能大家對Entry的印象是Map中實現的一個內部類,用來儲存鍵值對<key,value>,而在LinkedList中是要儲存<previous,item,next>,不便於凸顯Entry儲存鍵值對的特性吧,容易造成混淆。(這只是個人的猜測,若有不同見解可以交流)
補充:不論是Entry還是Node,都是外部類LinkedList實現的一個靜態內部類,這麼做是把一個類相關的型別放到內部,提高類的高內聚,而且通常情況下只有該外部類會呼叫其內部類,如果把Entry或者Node放到外部,明顯就提高了耦合性,對於其他集合型別的內部實現來說都是不利的。
再有一個,內部類會隨著外部類的載入而產生。
傳送門:關於靜態內部類
一、LinkedList概述
在原始碼中對LinkedList是這麼描述的:
- 雙向連結串列實現 List和Deque介面。實現所有可選的列表操作,並允許所有元素null。
- 所有操作的執行方式與雙向連結串列都是一樣的。索引到列表中的操作將從開始或結束遍歷列表,無論哪個更接近指定的索引。
- 此實現未同步。
*此類的 iterator和listIterator方法返回的迭代器:如果在建立迭代器之後的任何時間對結構進行修改,除了通過迭代器自己的remove}或{@code add方法,迭代器將丟擲一個ConcurrentModificationException。因此,面對併發修改,迭代器快速而乾淨地失敗,而不是在將來的未確定時間冒任意的,非確定性行為的風險。
二、LinkedList的繼承、實現關係
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- 繼承自AbstractSequentialList,而AbstractSequentialList父類為AbstractList,AbstractSequentialList 實現了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)這些骨幹性函式。
- 實現List介面,能對它進行佇列操作。
- 實現Deque介面,而Deque是Queue的子介面。Queue是一種佇列形式,而Deque則是雙向佇列,它支援從兩個端點方向檢索和插入元素,因此Deque既可以支援LIFO形式也可以支援LIFO形式。Deque介面是一種比Stack和Vector更為豐富的抽象資料形式,因為它同時實現了以上兩者。傳送門:Deque雙端佇列
- 實現了Cloneable介面,即覆蓋了函式clone(),能克隆。
- 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸
三、LinkedList屬性宣告及建構函式
transient int size = 0;
transient Node<E> first;//指向第一個節點的指標
transient Node<E> last;//指向最後一個節點的指標
//構造一個空列表
public LinkedList() {
}
//按照集合的迭代器返回的順序構造包含指定集合的元素的列表
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
—addAll()方法
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
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;
}
帶Collection值的構造方法的執行邏輯:
1)使用this()呼叫預設的無參建構函式;
2)呼叫addAll()方法,傳入當前的節點個數size,此時size為0,並將collection物件傳遞進去;
3)檢查index有沒有陣列越界的嫌疑;
4)將collection轉換成陣列物件a;
5)迴圈遍歷a陣列,然後將a陣列裡面的元素建立成擁有前後連線的節點,然後一個個按照順序連起來;
6)修改當前的節點個數size的值;
7)操作次數modCount自增1。
四、LinkedList的方法
(一)新增元素
—在頭部新增元素
//在此列表的開頭插入指定的元素
public void addFirst(E e) {
linkFirst(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++;
}
linkFirst(E e)是一個私有方法,所以無法在外部程式中呼叫(當然,這是一般情況,你可以通過反射上面的還是能呼叫到的)。
linkFirst(E e)首先構造一個變數結點f = first,再 new一個newNode(為要新增進來的節點),其前驅引用previous為null,後繼引用為f,再另頭結點指向新的節點newNode。
判斷,如果f == null,即列表為空,則頭尾節點指向同一個節點newNode;如果不為空,原來頭結點的前驅引用指向新節點newNode。
—在尾部新增元素
//將指定的元素追加到此列表的末尾
public void addLast(E e) {
linkLast(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++;
}
原理與在頭部新增元素類似,可參照上面進行解讀。
—在任意位置新增元素
// 在此列表中指定的位置插入指定的元素。將當前在該位置的元素(如果有)和任何後續元素向右移(將一個新增到它們的索引)
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//在非空節點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++;
}
從原始碼中看出,若索引index==size,便直接在尾部新增元素;若不是,則呼叫linkBefore(E e, Node<E> succ)函式。
linkBefore(E e, Node<E> succ)首先構造一個變數結點pred = succ.prev,再 new一個newNode(為要新增進來的節點),其前驅引用previous為pred ,後繼引用為succ,再另結點succ的前驅指向新的節點newNode。
判斷,如果pred == null,即列表為空,則頭尾節點指向同一個節點newNode;如果不為空,原來pred結點的後繼引用指向新節點newNode。
(二)檢視元素
檢視元素使用get方法。getFirst()、getLast()分別返回頭結點和尾節點。下面主要看看返回指定索引的方法get(int index)。
//返回此列表中指定位置的元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < 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;
}
}
get(int index)首先判斷給定索引是否存在,若存在執行node(index).item;其中item為元素的內容。
node(int index)方法返回的是一個節點Node<E>,程式碼中使用了類似二分法的查詢方法來遍歷元素。若index < (size >> 1),即索引在前半部分,則從前向後依次查詢;否則索引就在後半部分,從後向前依次查詢。
此段程式碼能夠有效的提高遍歷效率,也反映了雙向連結串列的優點——雙向連結串列增加了一點點的空間消耗(每個Node裡面還要維護它的前置Node的引用),同時也增加了一定的程式設計複雜度,卻大大提升了遍歷效率(體現在可以雙向遍歷)。
(三)刪除元素
removeFirst(),removeLast()分別用來刪除頭結點和尾結點,public E remove()方法刪除的也是列表的第一個元素,但是列表為空時使用不會丟擲異常(removeFirst()會丟擲異常)。
和ArrayList一樣,LinkedList支援按元素刪除和按下標刪除,下面我們主要介紹public E remove(int index),public boolean remove(Object o)
//刪除此列表中指定位置的元素。將任何後續元素向左移(從它們的索引中減去一個)。返回從列表中刪除的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//取消連結非空節點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;
}
按索引刪除remove(int index):
首先通過遍歷node(index)得到指定索引的節點,後通過unlink()方法進行刪除。
(1)x.prev = null;//前驅設定為null
(2)x.next = null;//後繼設定為null
(3)x.item = null;//內容設定為null
至此節點x為空節點,最後交給虛擬機器gc完成回收,刪除操作結束。
//從列表中刪除指定元素的第一次出現(如果存在)。如果此列表不包含元素,則不會更改。
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;
}
按元素刪除內容remove(Object o):
不論元素內容為空還是不為空,均通過節點的遍歷,依次查詢,若找到與指定內容一致的節點則刪除並返回。
注意:該方法從列表中刪除第一次出現的指定元素。
LinkedList的方法比較簡單,沒有擴容環節,翻閱原始碼基本能懂,不存在什麼大問題。由於LinkedList實現了Deque介面,該介面比List提供了更多的方法,包括 offer(),peek(),poll()等。
//檢索,但不刪除此列表的頭(第一個元素)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//檢索並刪除此列表的頭(第一個元素)
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//檢索但不刪除此列表的第一個元素,如果此列表為空,則返回null
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//檢索但不刪除此列表的最後一個元素,如果此列表為空,則返回null
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
//檢索並刪除此列表的第一個元素,如果此列表為空,則返回null
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//檢索並刪除此列表的最後一個元素,如果此列表為空,則返回null
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
//將指定的元素新增為此列表的尾部(最後一個元素)
public boolean offer(E e) {
return add(e);
}
//在此列表的前面插入指定的元素
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
//在此列表的結尾插入指定的元素
public boolean offerLast(E e) {
addLast(e);
return true;
}
//將元素推送到此列表所表示的堆疊。換句話說,將元素插入此列表的前面,此方法等效於addFirst
public void push(E e) {
addFirst(e);
}
//此列表所表示的堆疊中彈出一個元素。換句話說,刪除並返回此列表的第一個元素,此方法等效於removeFirst()
public E pop() {
return removeFirst();
}
五、ArrayList與LinkedList的區別
(一)從插入、刪除元素分析
對於兩者的插入、刪除操作不能片面的蓋棺定論,應視情況而定,下面以插入操作做分析(刪除操作的分析類似)
順序插入:
- ArrayList在不擴容的情況下順序插入速度較快,因為在構造ArrayList之前已經分配好空間,順序插入元素只是往指定記憶體空間補個元素;在需要擴容的情況下,ArrayList的順序插入則顯得比較慢,因為底層需要執行copy操作,既耗時又耗空間。
- LinkedList順序新增元素會教慢點,因為每新增一個元素都要新new一個節點物件,並且還有執行其他的引用賦值操作。
中間插入:
- ArrayList在執行中間插入的過程中耗時的是索引後面的元素copy移動,若果插入的位置越靠前則越慢,反之越快。
- LinkedList在任何位置插入的效率基本上是一致的,耗時的部分主要是定位索引(定址),賦值部分只需修改引用。
綜合以上所述:
(1)LinkedList做插入、刪除的時候,慢在定址,快在只需要改變前後Node的引用地址。
(2)ArrayList做插入、刪除的時候,慢在陣列元素的批量copy,快在定址。
所以,如果待插入、刪除的元素是在資料結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往後,對於LinkedList來說,因為它是雙向連結串列,所以在第2個元素後面插入一個資料和在倒數第2個元素後面插入一個元素在效率上基本沒有差別,但是ArrayList由於要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList。
(二)從遍歷列表分析
未完待續。。。
相關文章
- Java 集合系列之 LinkedList原始碼分析Java原始碼
- 死磕 java集合之LinkedList原始碼分析Java原始碼
- JAVA集合:LinkedList原始碼解析Java原始碼
- List集合總結,對比分析ArrayList,Vector,LinkedList
- 【Java集合原始碼剖析】LinkedList原始碼剖析Java原始碼
- Vector和Stack原始碼分析/List集合的總結原始碼
- 【集合框架】JDK1.8原始碼分析之LinkedList(七)框架JDK原始碼
- LinkedList原始碼分析原始碼
- Java集合之LinkedList原始碼解析Java原始碼
- Java List 常用集合 ArrayList、LinkedList、VectorJava
- Java集合原始碼探究~ListJava原始碼
- Java容器 | 基於原始碼分析List集合體系Java原始碼
- List集合(ArrayList-LinkedList);Set集合(HashSet-TreeSet)
- LinkedList原始碼分析(一)原始碼
- 原始碼分析之 LinkedList原始碼
- LinkedList原始碼分析(二)原始碼
- Java LinkedList 原始碼分析Java原始碼
- Java集合原始碼學習(3)LinkedListJava原始碼
- 集合框架原始碼學習之LinkedList框架原始碼
- java中的List介面(ArrayList、Vector、LinkedList)Java
- List原始碼分析原始碼
- LinkedList詳解-原始碼分析原始碼
- LinkedList與Queue原始碼分析原始碼
- jdk原始碼分析之LinkedListJDK原始碼
- .net原始碼分析 – List原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- 【JavaSE】集合類Collection集合Map集合的簡單介紹,List介面,中三個常用子類ArrayList、Vector、LinkedList之間的比較。Set介面。Java
- Java容器系列-LinkedList 原始碼分析Java原始碼
- LinkedList原始碼分析(jdk1.8)原始碼JDK
- java基礎:LinkedList — 原始碼分析Java原始碼
- 從面試角度分析LinkedList原始碼面試原始碼
- ArrayList、LinkedList和Vector的原始碼解析,帶你走近List的世界原始碼
- Java 基礎(四)集合原始碼解析 ListJava原始碼
- 小白學集合之List介面
- Java類集框架 —— LinkedList原始碼分析Java框架原始碼
- Java容器類框架分析(2)LinkedList原始碼分析Java框架原始碼