LinkedList 是 Java 集合框架中一個重要的實現,咱們先簡述一下LinkedList的一些特色:java
LinkedList
底層採用的雙向鏈表
結構;LinkedList
支持空值和重複值(List的特色);LinkedList
實現Deque接口,具備雙端隊列的特性,也能夠做爲棧來使用;LinkedList
存儲元素過程當中,無需像 ArrayList 那樣進行擴容,但存儲元素的節點須要額外的空間存儲前驅和後繼的引用;LinkedList
在鏈表頭部和尾部插入效率比較高,但在指定位置進行插入時,須要定位到該位置處的節點,此操做的時間複雜度爲O(N)
;LinkedList
是非線程安全的集合類,併發環境下,多個線程同時操做 LinkedList,會引起不可預知的異常錯誤。 直接經過idea查看一下LinkedList的繼承體系,體系結構比較複雜,一點點看。node
這裏簡單說一下AbstractSequentialList這個類,該類提供一套基本的基於順序訪問的接口,經過繼承此類,子類僅需實現部分代碼便可擁有完整的一套訪問某種序列表(好比鏈表)的接口。AbstractSequentialList 提供的方法基本上都是經過 ListIterator 實現的,好比下面的get和add方法。可是雖然LinkedList 繼承了 AbstractSequentialList,卻並無直接使用父類的方法,而是從新實現了一套的方法,後面咱們會講到這些方法的實現。面試
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
// 留給子類實現
public abstract ListIterator<E> listIterator(int index);
複製代碼
另外的就是文章開頭概述的,LinkedList實現了Deque接口,具備雙端隊列的特色。數組
//記錄鏈表中的實際元素個數
transient int size = 0;
//維護鏈表的首結點引用
transient Node<E> first;
//維護鏈表的尾節點引用
transient Node<E> last;
複製代碼
能夠看到first和last都是Node類型的,因此咱們簡單看一下LinkedList
中的這個內部類安全
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;
}
}
複製代碼
能夠看到Node這個靜態內部類的結構也是比較簡單的,每一個結點維護的就是本身存儲的元素信息+前驅結點引用+後繼節點引用。這裏就不作過多的闡述,下面簡單看看LinkedList
的構造方法併發
//構造一個空的集合(鏈表爲空)
public LinkedList() {
}
//先調用本身的無參構造方法構造一個空的集合,而後將Collection集合中的全部元素加入該鏈表中
//若是傳入的Collection爲空,會拋出空指針異常
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
複製代碼
LinkedList實現的添加方法主要有下面幾種框架
在鏈表尾部添加結點(linkLast方法)ide
在鏈表首部添加元素(linkFirst方法)源碼分析
在鏈表中間添加元素(linkBefore方法)post
下面咱們看看這三種方法的實現。
public void addLast(E e) {
linkLast(e);
}
複製代碼
在addLast方法中直接就是調用了linkLast方法實現結點的添加(沒有返回值,因此add方法必定是返回true的),因此下面咱們看看這個方法:
void linkLast(E e) {
//(1)得到當前鏈表實例的全局後繼節點
final Node<E> l = last;
//(2)建立一個新的結點,從Node的構造方法咱們就能知道
//這個新的結點中存放的元素item爲當前傳入的泛型引用,前驅結點爲全局後繼結點,後繼節點爲null
//(即至關於要將這個新節點做爲鏈表的新的後繼節點)
final Node<E> newNode = new Node<>(l, e, null);// Node(Node<E> prev, E element, Node<E> next){}
//(3)更新全局後繼節點的引用
last = newNode;
//(4)若是原鏈表的後繼結點爲null,那麼也須要將全局頭節點引用指向這個新的結點
if (l == null)
first = newNode;
//(5)不爲null,由於是雙向鏈表,建立新節點的時候只是將newNode的prev設置爲原last結點。這裏就須要將原last
//結點的後繼結點設置爲newNode
else
l.next = newNode;
//(6)更新當前鏈表中的size個數
size++;
//(7)這裏是fast-fail機制使用的參數
modCount++;
}
複製代碼
咱們經過一個示例圖來簡單模擬這個過程
該方法是一個private方法,經過addFirst方法調用暴露給使用者。
public void addFirst(E e) {
linkFirst(e);
}
複製代碼
咱們仍是主要看看linkFirst方法的實現邏輯
private void linkFirst(E e) {
//(1)獲取全局頭節點
final Node<E> f = first;
//(2)建立一個新節點,其前驅結點爲null,後繼結點爲當前的全局首結點
final Node<E> newNode = new Node<>(null, e, f);
//(3)更新全局首結點引用
first = newNode;
//(4)若是首結點爲null,last結點指向新建的結點
if (f == null)
last = newNode;
//(5)不爲null,原頭節點的前驅結點爲newNode
else
f.prev = newNode;
size++;
modCount++;
}
複製代碼
上面的邏輯也比較簡單,就是將新添加的結點設置爲頭節點,而後更新鏈表中結點之間的指向,咱們經過下面這個圖簡單理解一下(鏈表初始爲null就不作演示了,和上面圖示的差很少,這裏假設已經存在結點)
public void add(int index, E element) {
//檢查index的合法性:大於等於0小於等於size,不合法會拋出異常
checkPositionIndex(index);
//index等於size,就在尾部插入新節點,linkLast方法上面說到過
if (index == size)
linkLast(element);
//不然就在指定index處插入結點,先找到index處的結點(調用的是node(index方法))
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
複製代碼
add(index,element)方法中主要的邏輯仍是linkBefore,咱們下面看看這個方法,在此以前調用的是node(index)方法,找到index處的結點
Node<E> node(int index) {
//index < size/2 (index在鏈表的前半部分)
if (index < (size >> 1)) {
//使用全局頭節點去查找(遍歷鏈表)
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//index > size / 2 (index在鏈表的後半部分)
Node<E> x = last;
//使用全局尾節點向前查找
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
複製代碼
node方法實現利用雙向鏈表以及記錄了鏈表總長度的這兩個特色,分爲先後兩部分去遍歷查詢jindex位置處的結點。查找這個結點後,就會做爲參數調用linkBefore方法,以下所示
void linkBefore(E e, Node<E> succ) {
//succ != null;succ就是指定位置處的結點
//傳入的結點element=succ
final Node<E> pred = succ.prev;
//建立新的結點
//前驅結點是傳入的結點的前驅結點
//後繼結點是傳入的結點
final Node<E> newNode = new Node<>(pred, e, succ);
//更新index處結點的前驅結點引用
succ.prev = newNode;
//index處結點的前驅結點爲null,那麼就至關於在頭部插入結點,而且更新first
if (pred == null)
first = newNode;
//不爲null,那麼它的後繼結點就是新的結點
else
pred.next = newNode;
size++;
modCount++;
}
複製代碼
這個方法的邏輯也比較簡單,就是在succ和succ.prev兩個結點之間插入一個新的結點,咱們經過簡單的圖示理解這個過程
做爲雙端隊列,刪除元素也有兩種方式隊列首刪除元素
、隊列尾刪除元素
;做爲List,又要支持中間刪除元素
,因此刪除元素一個有三個方法。
下面是調用unlinkFirst方法的兩個public方法(Deque接口的方法實現),主要區別就是removeFirst方法執行時候,first爲null的時候會拋出異常,而pollFirst返回null。
// remove的時候若是沒有元素拋出異常
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// poll的時候若是沒有元素返回null
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
複製代碼
主要仍是看unlinkFirst這個方法的實現
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//獲取頭結點的元素值
final E element = f.item;
//獲取頭結點的後繼結點
final Node<E> next = f.next;
//刪除頭節點中存放的元素item和後繼結點,GC
f.item = null;
f.next = null; // help GC
//更新頭節點引用(原頭節點的後繼結點)
first = next;
//鏈表中只有一個結點,那麼尾節點也是null了
if (next == null)
last = null;
//將新的頭節點的前驅結點設置爲null
else
next.prev = null;
//更新size和modCount
size--;
modCount++;
//返回原頭節點的值
return element;
}
複製代碼
下面是調用unlinkLast方法的兩個public方法(Deque接口的方法實現),主要區別就是removeLast方法執行時候,first爲null的時候會拋出異常,而pollLast返回null。
// remove的時候若是沒有元素拋出異常
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// poll的時候若是沒有元素返回null
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
複製代碼
下面是unlinkLast方法的實現
// 刪除尾節點
private E unlinkLast(Node<E> l) {
// 尾節點的元素值
final E element = l.item;
// 尾節點的前置指針
final Node<E> prev = l.prev;
// 清空尾節點的內容,協助GC
l.item = null;
l.prev = null; // help GC
// 讓前置節點成爲新的尾節點
last = prev;
// 若是隻有一個元素,刪除了把first置爲空
// 不然把前置節點的next置爲空
if (prev == null)
first = null;
else
prev.next = null;
// 更新size和modCount
size--;
modCount++;
// 返回刪除的元素
return element;
}
複製代碼
// 刪除中間節點
public E remove(int index) {
// 檢查是否越界
checkElementIndex(index);
// 刪除指定index位置的節點
return unlink(node(index));
}
複製代碼
// 刪除指定節點x
E unlink(Node<E> x) {
// x的元素值
final E element = x.item;
// x的前置節點
final Node<E> next = x.next;
// x的後置節點
final Node<E> prev = x.prev;
// 若是前置節點爲空
// 說明是首節點,讓first指向x的後置節點
// 不然修改前置節點的next爲x的後置節點
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 若是後置節點爲空
// 說明是尾節點,讓last指向x的前置節點
// 不然修改後置節點的prev爲x的前置節點
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 清空x的元素值,協助GC
x.item = null;
// 元素個數減1
size--;
// 修改次數加1
modCount++;
// 返回刪除的元素
return element;
}
複製代碼
LinkedList
底層基於鏈表結構,沒法向 ArrayList 那樣隨機訪問指定位置的元素。LinkedList 查找過程要稍麻煩一些,須要從鏈表頭結點(或尾節點)向後查找,時間複雜度爲 O(N)
。相關源碼以下:
public E get(int index) {
checkElementIndex(index); //仍是先檢驗index的合法性,這裏上面已經說過
//調用node方法遍歷查詢index處的結點,而後返回結點存放的值item,node方法上面已經說過
return node(index).item;
}
複製代碼
鏈表的遍歷過程也很簡單,和上面查找過程相似,咱們從頭節點日後遍歷就好了。但對於 LinkedList 的遍歷仍是須要注意一些,否則可能會致使代碼效率低下。一般狀況下,咱們會使用 foreach 遍歷 LinkedList,而 foreach 最終轉換成迭代器形式。因此分析 LinkedList 的遍歷的核心就是它的迭代器實現,相關代碼以下:
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;
/** 構造方法將 next 引用指向指定位置的節點 */
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;
}
//...other method
}
複製代碼
這裏主要說下遍歷 LinkedList 須要注意的一個點。LinkedList 不擅長隨機位置訪問,若是你們用隨機訪問的方式遍歷 LinkedList,效率會不好。好比下面的代碼:
List<Integet> list = new LinkedList<>();
list.add(1)
list.add(2)
......
for (int i = 0; i < list.size(); i++) {
Integet item = list.get(i);
// do something
}
複製代碼
當鏈表中存儲的元素不少時,上面的遍歷方式對於效率確定是很是低的。緣由在於,經過上面的方式每獲取一個元素(調用get(i)方法,上面說到了這個方法的實現
),LinkedList 都須要從頭節點(或尾節點)進行遍歷(node()方法的實現
),效率低,上面的遍歷方式在大數據量狀況下,效率不好。在平常使用中應該儘可能避免這種用法。
最後總結一下面試常問的ArrayList
和LinkedList
的區別,關於ArrayList
請參考我上一篇ArrayList源碼分析。
ArrayList
是基於動態數組實現的,LinkedList
是基於雙向鏈表實現的;
對於隨機訪問來講,ArrayList
(數組下標訪問)要優於LinkedList
(遍歷鏈表訪問);
不考慮直接在尾部添加數據的話,ArrayList
按照指定的index
添加/刪除數據是經過複製數組實現。LinkedList
經過尋址改變節點指向實現;因此添加元素的話LinkedList(改變結點的next和prev指向便可)要優於ArrayList(移動數組元素)。
LinkedList
在數據存儲上不存在浪費空間的狀況。ArrayList
動態擴容會致使有一部分空間是浪費的。