1. 程式人生 > >jdk1.8-LinkedList源碼分析

jdk1.8-LinkedList源碼分析

類的成員 簡單的 常用方法 屬性 方便 ransient 理解 ans 鏈表

一:類的繼承關系技術分享圖片
我們看下類的繼承關系
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
繼承抽象的AbstractSequentiaList類,供了一個基本的List接口實現,為實現序列訪問的數據儲存結構的提供了所需要的最小化的接口實現。.同時LinkedList也實現了Cloneable、java.io.Serializable、Deque(雙端隊列)等接口

二:類的成員屬性1.成員屬性比較簡單
/**
* 當前鏈表長度
*/
transient int size = 0;

/**
* 頭結點
*/
transient Node<E> first;

/**
* 尾結點
*/
transient Node<E> last;

2.這裏我們要看Node這個實體類,因為java是沒有指針這一說的,所以用Node自己來模擬實現
/**
* 靜態內部類,它的創建是不需要依賴於外圍類,可以被實例化
* @param <E>
*/
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;
}
}
分析:我們知道java的LinkedList是個雙向鏈表,java沒有指針,那麽是怎麽找到前一個元素結點和後一個元素結點呢?從上面的代碼很容易就可以看出,其實就是在Node類的成員屬性下直接記錄了下一個結點Node<E> next 和 上一個結點 Node<E> prev,從而達到模擬指針的效果。

三:構造方法1.不帶參的構造方法,啥也沒做
/**
* Constructs an empty list.
*/
public LinkedList() {
}


2.帶參構造方法
/**
* 帶參構造方法,傳入集合
*/
public LinkedList(Collection<? extends E> c) {
//構造方法
this();
//添加方法
addAll(c);
}
分析:這裏將集合直接傳入addAll()方法
/**
* 參數為前鏈表長度和集合
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
分析:這裏傳入了當前鏈表長度和集合
/**
* 從索引為index結點的尾部,開始插入所有集合的元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
//檢驗長度是否在鏈表長度區間
checkPositionIndex(index);
//將集合轉為數組
Object[] a = c.toArray();
//數組長度
int numNew = a.length;
//如果等於0直接返回false
if (numNew == 0)
return false;

//兩個記錄結點
Node<E> pred, succ;
//如果當前傳入的長度等於鏈表當前最大長度
if (index == size) {
//succ結點等於null
succ = null;
//pred結點等於尾結點
pred = last;
} else {
//返回index索引的結點
succ = node(index);
//pred記錄等於index索引結點的上一個結點
pred = succ.prev;
}

//遍歷數組
for (Object o : a) {
//將值強轉為E類型
@SuppressWarnings("unchecked") E e = (E) o;
//每次循環new一個newNode結點,newNode結點的值等於e,newNode上一結點prev等於pred記錄結點,newNode下一節點等於null
//其實這個newNode就是pred的下一個結點,因為newNode的上一節點等於pred
Node<E> newNode = new Node<>(pred, e, null);
//如果當前記錄結點pred等於null
if (pred == null)
//則頭結點等於newNode
first = newNode;
else
//當前記錄pred的下一節點等於newNode,相當於把succ記錄結點給覆蓋了
pred.next = newNode;
//當前記錄結點更新為newNode,也就是相當於鏈表往後移動一位
pred = newNode;
}

if (succ == null) {
//尾結點等於當前記錄結點pred
last = pred;
} else {
//重新把pred結點下一個結點賦值為succ記錄結點
pred.next = succ;
//並且讓succ記錄結點的上一個結點等於最新的pred記錄結點
succ.prev = pred;
}

//鏈表的長度加上集合的長度
size += numNew;
//修改次數加1
modCount++;
return true;
}
分析:首先我們要知道這個addAll()方法是從索引為index結點開始向後插入元素的,鏈表的索引是從0開始的。那麽到底是怎麽插入元素的呢?我們來逐步分析下:
//檢驗長度是否在鏈表長度區間
checkPositionIndex(index);
//將集合轉為數組
Object[] a = c.toArray();
//數組長度
int numNew = a.length;
//如果等於0直接返回false
if (numNew == 0)
return false;
分析:這裏這先校驗下傳入的index長度是否在鏈表區間(0到size),然後將集合轉為數組,如果數組長度等於0的話,直接返回了。
//兩個記錄結點
Node<E> pred, succ;
//如果當前傳入的索引等於鏈表當前最大長度
if (index == size) {
//succ結點等於null
succ = null;
//pred結點等於尾結點
pred = last;
} else {
//返回index索引的結點
succ = node(index);
//pred記錄等於index索引結點的上一個結點
pred = succ.prev;
}
分析:兩個記錄結點Node<E> pred,succ非常重要,它是為了方便我們我們操作鏈表的。如果,當前傳進來的索引index等於鏈表長度size,那麽succ結點等於null,為什麽呢?因為index=size下標已經超出鏈表了,所以值肯定為null了。也因此,我們可以理解succ其實就是索引為index的結點,也就是我們要開始插入元素的結點,然後pred結點等於尾結點。從這可以看出,我們前面構造方法傳入當前鏈表長度和集合是從鏈表的尾部開始添加的(也就是尾結點的下一個結點),符合邏輯。
如果當前傳入的索引不大於鏈表長度size,succ等於index索引的結點,pred還是等於index索引的上一個結點。我們看下這個node(index)方法,看完之後我們明白它確實是返回index索引的結點,然後遍歷的時候有個小技巧,如果索引小於鏈表長度的一半從頭結點開始往後遍歷,否則從尾結點往前開始遍歷,節省時間。
/**
* 傳入index長度,鏈表返回索引為index的node結點(註意這裏的鏈表索引也是從0開始)
* 如果傳入的長度小於鏈表長度一遍,那麽從鏈表頭結點開始遍歷
* 如果傳入的長度大於或等於鏈表長度的一半,那麽從鏈表的尾結點開始遍歷
*/
Node<E> node(int index) {
//如果傳入的長度小於鏈表長度的一半(size >> 1鏈表長度除以2,這種寫法運算速度稍快,可以根據實際需求應用)
if (index < (size >> 1)) {
//當前記錄結點等於頭結點
Node<E> x = first;
//從下表為0開始遍歷到索引為index-1
//這裏註意下從0到index-1需要叠代7次
for (int i = 0; i < index; i++)
//當前記錄結點等於下一節點
x = x.next;
//所以返回的是索引為indx的點!!
return x;
} else {
//如果傳入的長度大於或者等於當前鏈表長度的一半
//當前記錄結點等於尾結點
Node<E> x = last;
//從鏈表末尾開始往前遍歷
for (int i = size - 1; i > index; i--)
//當前記錄結點等於上一節點
x = x.prev;
//返回索引為index的點!
return x;
}
}
到此我們搞明白了,
//兩個記錄結點
Node<E> pred, succ;
這兩個結點的含義,succ代表當前傳入索引index的結點,pred代表索引index結點的上一個結點,這兩個結點是為了我們方便操作鏈表。
接下來,我們繼續往下分析:
//遍歷數組
for (Object o : a) {
//將值強轉為E類型
@SuppressWarnings("unchecked") E e = (E) o;
//每次循環new一個newNode結點,newNode結點的值等於e,newNode上一結點prev等於pred記錄結點,newNode下一節點等於null
//其實這個newNode就是pred的下一個結點,因為newNode的上一節點等於pred
Node<E> newNode = new Node<>(pred, e, null);
//如果當前記錄結點pred等於null
if (pred == null)
//則頭結點等於newNode
first = newNode;
else
//當前記錄pred的下一節點等於newNode,相當於把succ記錄結點給覆蓋了
pred.next = newNode;
//當前記錄結點更新為newNode,也就是相當於鏈表往後移動一位
pred = newNode;
}
分析:前面將集合轉為了數組,這裏遍歷數組,每次循環將遍歷的值轉為類型E(泛型)。new一個名為newNode的結點,構造函數初始化,newNode結點的上一結點等於pred結點(pred代表索引index結點的上一個結點),newNode結點的值為e,newNode結點的下一節點為null。看懂了沒,哈哈,其實這個newNode相當於“覆蓋”了succ結點(當前索引為index的結點),這裏畫個圖給大家理解下吧。技術分享圖片newNode結點的上一結點等於pred結點,下一節點此時為null

if (pred == null)
//則頭結點等於newNode
first = newNode;
else
//當前記錄pred的下一節點等於newNode,相當於把succ記錄結點給覆蓋了
pred.next = newNode;
當pred == null時,鏈表的頭結點等於newNode。因為如果當前傳入索引index的結點succ為頭結點,那麽上一個結點pred就肯定會null,如下圖:
技術分享圖片pred不等於null時,pred結點的下一節點更改為newNode結點如下圖:技術分享圖片然後pred.next = newNode也就是當前傳入索引index的結點succ的上一個結點變為了newNode結點,以便下次再添加時,pred變為newNode,for循環一次結束,第二次循環的話,添加效果圖如下:技術分享圖片
如此,直至for循環結束。

然後,再往下看代碼
if (succ == null) {
//尾結點等於當前記錄結點pred
last = pred;
} else {
//重新把pred結點下一個結點賦值為succ記錄結點
pred.next = succ;
//並且讓succ記錄結點的上一個結點等於最新的pred記錄結點
succ.prev = pred;
}
分析:當傳入的index等於鏈表長度size時,鏈表索引是從0開始的,所以超出鏈表索引,那麽當前索引succ == null,則直接讓pred等於尾結點(很好理解吧?succ的上一個結點不就是索引為index-1的尾結點)。如果不等於null,把pred(此時的pred等於最後一個newNode結點)下一個結點指向succ結點,succ的上一個結點指向pred,效果如下圖:技術分享圖片
最後:
//鏈表的長度加上集合的長度
size += numNew;
//修改次數加1
modCount++;
return true;
分析:鏈表長度加上集合的長度,修改次數加1,返回
到此,addAll()方法分析結束了,總結下,我們明白了在添加鏈表元素時並不是真正所謂上的“添加”,這裏的添加其實是改變指針的指向從而達到往鏈表添加的目的,刪除鏈表元素也類似。

四:我們看下最常用的幾個方法1.往鏈表末尾添加單個元素方法
/**
* 往鏈表末尾添加元素
*/
public boolean add(E e) {
linkLast(e);
return true;
}
分析:傳入元素的值,調用linkLast()方法
/**
* Links e as last element.
*/
void linkLast(E e) {
//尾結點
final Node<E> l = last;
//new一個新結點,上一個結點等於尾結點,結點值等於e,下一個結點等於null
final Node<E> newNode = new Node<>(l, e, null);
//尾結點等於新結點
last = newNode;
//鏈表沒有元素,則尾結點為null
if (l == null)
//讓頭結點等於新結點
first = newNode;
else
//否則,尾結點的下一個結點等於新結點
l.next = newNode;
//鏈表長度加1
size++;
//修改次數加1
modCount++;
}
分析:這裏添加和上面思想其實是一致的,所以就不詳解了和畫圖了,直接看上面我寫的註釋。
2.根據索引刪除元素
/**
* 根據索引刪除結點
*/
public E remove(int index) {
//檢查索引是否越界
checkElementIndex(index);
//node(index)索引為index的node結點,傳入unLink()方法
return unlink(node(index));
}

繼續往下看unlink()方法,傳入的是索引為index的node結點對象
/**
* 刪除指定節點元素
*/
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;

//如果當前節點的上一節點prev為null
if (prev == null) {
//頭節點等於下一節點
first = next;
} else {
//上一節點成員變量下一節點改為next
prev.next = next;
//當前節點的上一節點置為null方便回收
x.prev = null;
}

//如果當前節點的下一節點next為null(也就是鏈表最後一個元素)
if (next == null) {
//鏈表的尾節點等於當前節點的上一節點prev
last = prev;
} else {
//當前節點的下一節點next的成員變量prev等於當前節點的上一節點prev
next.prev = prev;
//當前節點的下一節點置為null
x.next = null;
}

//當前節點的值置為null
x.item = null;
//當前鏈表長度減1
size--;
//修改次數加1
modCount++;
//返回當前節點元素值
return element;
}
分析:這裏的刪除指定節點,其實道理也是類似,通過改變上一節點和下一節點各自的成員變量引用從而達到刪除當前元素節點的效果。技術分享圖片




到此為止,LinkedList其它常用方法源碼不再進行分析了,基本掌握了核心的原理,其它的都是類似。這樣我們就簡單的看了下LinkedList源碼。下一篇分析HashMap源碼

jdk1.8-LinkedList源碼分析