LinkedList原始碼分析(jdk1.8)
一、概述
上圖為LinkedList的繼承結構圖,根據繼承關係總結如下:
LinkedList 是一個繼承於AbstractSequentialList的雙向連結串列。它也可以被當作堆疊、佇列或雙端佇列進行操作。
LinkedList 實現 List 介面,能對它進行佇列操作。
LinkedList 實現 Deque 介面,即能將LinkedList當作雙端佇列使用。
LinkedList 實現了Cloneable介面,即覆蓋了函式clone(),能克隆。
LinkedList 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸。
LinkedList 是非同步的。
二、原始碼分析
1、成員變數
//集合元素數量
transient int size = 0;
//連結串列頭結點
transient Node<E> first;
//連結串列尾結點
transient Node<E> last;
LinkedList的屬性不多,size表示其元素數量,first表示連結串列頭節點,而last表示連結串列尾節點。其中連結串列是由節點(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; } }
結點(Node)為LinkedList的靜態內部類,且有前置節點和後置節點 ,只是頭節點的前置節點為空,而尾節點的後置節點為空,說明LinkedList內部為雙向連結串列結構。可以用如下的圖形象的表示:
2、構造方法
//預設構造方法 public LinkedList() { } //通過集合c的元素構造連結串列 public LinkedList(Collection<? extends E> c) { this(); addAll(c);//將集合c中的所有元素新增到LinkedList中 }
不傳參的構造方法建立空的連結串列,將集合作為入參的構造方法則通過集合c的元素建立連結串列,呼叫了addAll方法,addAll方法的原理將在下面講解。
3、常用方法
我們從LinkedList集合的常用操作入手,比如集合的增刪改查,棧的進棧和出棧,佇列的進佇列和出佇列等。
3.1、增
方法名 | 功能 |
---|---|
linkFirst(E e) | 新增元素e到連結串列的頭部 |
linkLast(E e) | 新增元素e到連結串列的尾部 |
addFirst(E e) | 新增元素e到連結串列的頭部,呼叫 linkFirst方法,無返回值 |
addLast(E e) | 新增元素e到連結串列的尾部,呼叫linkLast方法,無返回值 |
add(E e) | 新增元素e到連結串列的尾部,呼叫linkLast方法 |
add(int index, E element) | 新增元素e到連結串列的指定位置index處 |
offer(E e) | 新增元素e到連結串列的尾部,呼叫 add(E e) 方法 |
offerFirst(E e) | 新增元素e到連結串列的頭部,呼叫 addFirst(E e)方法,返回true或false |
offerLast(E e) | 新增元素e到連結串列的尾部,呼叫 addLast(E e)方法,返回true或false |
addAll(Collection<? extends E> c) | 新增集合c的所有元素到連結串列的尾部 |
addAll(int index, Collection<? extends E> c) | 新增集合c的所有元素到連結串列的指定位置 處 |
linkBefore(E e, Node succ) | 將元素e插入到連結串列的指定節點succ前 |
push(E e)(E e, Node succ) | 將元素e插入到棧上,無返回值,其實就是呼叫了addFirst(E e)方法 |
有上面的表格可知,只要理解了linkFirst,linkLast方法的原始碼,大部分方法的原理都搞懂了,首先來看下linkFirst,linkLast的原始碼:
//連結串列的頭部插入元素e
private void linkFirst(E e) {
//獲取連結串列的頭節點
final Node<E> f = first;
//通過元素e建立新節點,其前置節點為空,後置結點為當前連結串列的頭節點
final Node<E> newNode = new Node<>(null, e, f);
//將連結串列的頭結點設定為新節點
first = newNode;
//若原本的頭節點為空,說明為空連結串列,將尾結點也設定為新節點;若原本的頭節點不為空,就將其前置結點設定為新節點;
//此時完成了頭部插入節點的操作
if (f == null)
last = newNode;
else
f.prev = newNode;
//將元素大小和連結串列修改記錄分別+1
size++;
modCount++;
}
//連結串列的尾部插入元素e
void linkLast(E e) {
//獲取連結串列的頭結點
final Node<E> l = last;
//通過元素e建立新節點,其前置結點當前列表的尾節點,後置節點為空
final Node<E> newNode = new Node<>(l, e, null);
//將連結串列的尾結點設定為新節點
last = newNode;
//若原本的尾節點為空,說明為空連結串列,將頭結點也設定為新節點;若原本的尾節點不為空,就將其後置結點設定為新節點;
//此時完成了尾部插入結點的操作
if (l == null)
first = newNode;
else
l.next = newNode;
//將元素大小和連結串列修改記錄分別+1
size++;
modCount++;
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
public E offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public void push(E e) {
addFirst(e);
}
add(int index, E element) :在指定位置新增元素
在add(int index, E element)方法中呼叫了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;
}
}
瞭解了node(int index)後再進一步看add(int index, E element) 方法的執行過程
public void add(int index, E element) {
//檢測下標的邊界是否合法
checkPositionIndex(index);
if (index == size)
//若index == size說明往尾部插入元素,直接呼叫linkLast方法;否則呼叫linkBefore方法
linkLast(element);
else
linkBefore(element, node(index));
}
//將元素e插入到連結串列的指定節點succ前
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++;
}
add(int index, E element)的執行過程可以用下圖表示,可以知道LinkedList的隨機插入效率比ArrayList高,因為ArrayList需要移動陣列中相應的元素而它不需要。
addAll方法
addAll(Collection<? extends E> c):在尾部插入集合c中的元素
addAll(int index, Collection<? extends E> c):在指定位置index插入集合c中的元素
//在尾部插入集合c中的元素
public boolean addAll(Collection<? extends E> c) {
//呼叫addAll(int index, Collection<? extends E> c)方法,當index=size時就是從尾部位置插入
return addAll(size, c);
}
//在指定位置index插入集合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;
//index節點的前置節點,後置節點
Node<E> pred, succ;
//當index=size時就是從尾部位置插入,此時後置節點設為空,前置節點設為當前連結串列的尾節點
//否則表示從指定位置插入,此時後置節點設為指定下標的節點,前置節點設為指定下標節點的前置節點
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) {//迴圈結束後,判斷,如果後置節點是null。 說明此時是在隊尾append的。
last = pred;//將尾節點設定為後置節點
} else {
pred.next = succ;//否則是在隊中插入的節點 ,更新前置節點的後置節點
succ.prev = pred;//再更新後置節點的前置節點
}
size += numNew;
modCount++;
return true;
}
addAll方法主要分為4步:
- 檢查index索引的邊界值
- 將集合資料 轉換為陣列
- 得到插入位置index的前置節點和後置節點
- 遍歷資料,將資料插入到指定位置
3.2、刪
方法名 | 功能 |
---|---|
unlink(Node x) | 刪除節點 |
removeFirst | 刪除首節點並返回該刪除首節點,呼叫方法 unlinkFirst(Node f) |
removeLast | 刪除尾節點並返回該刪除尾節點,呼叫方法 unlinkLast(Node l) |
remove(int index) | 刪除指定位置節點並返回該刪除節點 |
remove(Object o) | 刪除首次出現的指定的節點,刪除成功返回true,不存在返回false |
removeFirstOccurrence(Object o) | 刪除首次出現的指定的節點,刪除成功返回true,不存在返回false,呼叫方法remove(Object o) |
removeLastOccurrence(Object o) | 刪除最後一次出現的指定的節點,刪除成功返回true,不存在返回false |
poll | 檢索並刪除此連結串列的頭節點,不存在返回空 |
pollFirst | 檢索並刪除此連結串列的頭節點,不存在返回空 |
pollLast | 檢索並刪除此連結串列的尾節點,不存在返回空 |
unlink(Node x):刪除指定節點x
//取消連結串列的非空節點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;
}
removeFirst:刪除首節點並返回該刪除首節點,呼叫方法 unlinkFirst(Node f)
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(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; //首節點置為空,剩下的交給GC處理
first = next;//將連結串列的首節點設為後置節點
if (next == null)
last = null;//若後置節點為空,說明為尾節點,此時將尾節點設為空
else
next.prev = null;//若後置節點不為空,將其前置節點設為空,此時相當於變成了首節點
size--;
modCount++;
return element;
}
removeLast :刪除尾節點並返回該刪除尾節點,呼叫方法 unlinkLast(Node l)
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(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; //尾節點置為空,剩下的交給GC處理
last = prev;//將連結串列的尾節點設為前置節點
if (prev == null)
first = null;//若前置節點為空,說明為首節點,此時將首節點設為空
else
prev.next = null;//若前置節點不為空,將其後置節點設為空,此時相當於變成了尾節點
size--;
modCount++;
return element;
}
remove(int index): 刪除指定位置節點並返回該刪除節點
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
刪除流程主要為:
1.得到待刪除節點的前置節點和後置節點
2.刪除前置節點
3.刪除後置節點
remove(Object o):刪除首次出現的指定的節點,刪除成功返回true,不存在返回false。
該方法就是遍歷元素然後匹配的話就呼叫刪除節點方法unlink(Node x)。
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.3、改
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;
}
3.4、查
方法名 | 功能 |
---|---|
node(int index) | 獲取指定位置處的節點 |
get(int index) | 獲取指定位置處的元素 |
getFirst | 獲取連結串列的第一個元素 |
getLast | 獲取連結串列的最後一個元素 |
element | 獲取連結串列的第一個元素,呼叫方法getFirst |
peek | 獲取連結串列的第一個元素 ,不存在則返回空 |
peekFirst | 獲取連結串列的第一個元素 ,不存在則返回空 |
peekLast | 獲取連結串列的最後一個元素 ,不存在則返回空 |
node(int index)方法在前面已經提過,它通過二分法的方式縮小遍歷範圍,一定程度上提高了檢索效率,不過肯定比不上ArrayList的檢索效率。
get(int index):獲取指定位置處的節點
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
getFirst:獲取連結串列的第一個元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
getLast:獲取連結串列的最後一個元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
三、總結
- LinkedList不是執行緒安全的。
- LinkedList底層基於雙向連結串列,隨機插入和刪除效率比ArrayList高,但跟據索引查詢的效率比ArrayList低。
- LinkedList相比ArrayList沒有擴容操作,ArrayList可能會浪費一定的容量空間,但LinkedList每個節點都要儲存前後節點的引用,需要消耗相當的空間,就儲存密度來說,ArrayList優於LinkedList。
- 可以使用LinkedList作為佇列和棧的實現。
參考連結:
https://www.imooc.com/article/78970
https://blog.csdn.net/zxt0601/article/details/77341098
https://blog.csdn.net/qq_19431333/article/details/54572876
https://www.cnblogs.com/xujian2014/p/4630785.html#_label2