java集合原始碼分析(四)---LinkedList
吐槽
今天看到別人說的一句話感觸蠻深的一個人把時間用到哪裡,他的成就在那裡 自己最近真的蠻浮躁的,對自己的能力莫名其妙的錯誤估計,但實際上自己的真實的水平什麼的自己還是要有點B數,既然選擇這條路的話,還是好好的自己一步一步走下去吧。
LinkedList
這個是Collection陣營的一個集合
其實和ArrayList類似,但是其底層的實現是用雙向連結串列來進行實現的
我們還是先想幾個問題帶著問題去學習
- LinkedList的底層用什麼實現的?
- LinkedList的插入刪除的時候的高效如何實現?
- LinkedList的執行緒是否安全
- LinkedList和ArrayList的比較
-List,ArrayList,LinkedList的區別
原始碼分析
成員變數和繼承關係
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable {
//集合元素的數量
transient int size;
//連結串列的頭結點
transient LinkedList.Node<E> first;
//連結串列的尾結點
transient LinkedList.Node< E> last;
private static final long serialVersionUID = 876323262645176354L;
先看下繼承關係
LinkedList 繼承自 AbstractSequentialList 介面,同時了還實現了 Deque, Queue 介面。
我們看到成員變數就三個,簡單明瞭
- 頭結點
- 尾結點
- 集合元素的個數
然後我們去看下結點類
private static class Node<E> {
//元素的值
E item;
//後繼結點
LinkedList.Node<E> next;
//前驅結點
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> var1, E var2, LinkedList.Node<E> var3) {
this.item = var2;
this.next = var3;
this.prev = var1;
}
}
發現這塊是兩個結點
一個前驅結點和一個後繼結點
所以,LinkedList是一個雙向連結串列
構造方法
這個比ArrayList簡單清晰多了
如果是傳入的空的話,集合的元素為0
public LinkedList() {
this.size = 0;
}
//如果傳入的是集合的話
//用addAll把集合全部加入連結串列中
public LinkedList(Collection<? extends E> var1) {
this();
this.addAll(var1);
}
allAll()全部新增
看下這塊的過程
//傳入一個集合的時候
public LinkedList(Collection<? extends E> var1) {
this();
this.addAll(var1);
}
//以size為下標,插入集合的所有元素
public boolean addAll(Collection<? extends E> var1) {
return this.addAll(this.size, var1);
}
//傳入下標,和集合物件
public boolean addAll(int var1, Collection<? extends E> var2) {
//先檢查是否越界 看了下呼叫 就是在區間【0,size】中沒
this.checkPositionIndex(var1);
//然後把集合物件全部轉換成陣列
Object[] var3 = var2.toArray();
//獲取到陣列的長度
int var4 = var3.length;
//如果陣列是0的話放回false,不添加了
if (var4 == 0) {
return false;
} else {
//前置結點
LinkedList.Node var5;
//後置結點
LinkedList.Node var6;
//在連結串列尾部追加資料
if (var1 == this.size) {
var6 = null;//尾部資料一定是null
var5 = this.last;//前置結點是隊尾
} else {
var6 = this.node(var1);
var5 = var6.prev;
}
Object[] var7 = var3;
int var8 = var3.length;
//批量新增
for(int var9 = 0; var9 < var8; ++var9) {
Object var10 = var7[var9];
LinkedList.Node var12 = new LinkedList.Node(var5, var10, (LinkedList.Node)null);
if (var5 == null) {
this.first = var12;
} else {
var5.next = var12;
}
var5 = var12;
}
if (var6 == null) {
this.last = var5;
} else {
var5.next = var6;
var6.prev = var5;
}
this.size += var4;
++this.modCount;
return true;
}
}
private void checkPositionIndex(int var1) {
if (!this.isPositionIndex(var1)) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
}
}
private boolean isPositionIndex(int var1) {
return var1 >= 0 && var1 <= this.size;
}
//根據index 查詢出Node,
LinkedList.Node<E> node(int var1) {
LinkedList.Node var2;
int var3;
if (var1 < this.size >> 1) {
var2 = this.first;
for(var3 = 0; var3 < var1; ++var3) {
var2 = var2.next;
}
return var2;
} else {
var2 = this.last;
for(var3 = this.size - 1; var3 > var1; --var3) {
var2 = var2.prev;
}
return var2;
}
}
get和set方法
public E get(int var1) {
this.checkElementIndex(var1);
return this.node(var1).item;
}
public E set(int var1, E var2) {
this.checkElementIndex(var1);
LinkedList.Node var3 = this.node(var1);
Object var4 = var3.item;
var3.item = var2;
return var4;
}
邏輯都是很簡單的那種,我們發現這塊就是呼叫node()方法
//函式會以O(n/2)的效能去獲取一個節點
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;
}
}
就是判斷index是在前半區間還是後半區間,如果在前半區間就從head搜尋,而在後半區間就從tail搜尋。而不是一直從頭到尾的搜尋。如此設計,將節點訪問的複雜度由O(n)變為O(n/2)。
真的是設計的很巧妙
刪除
public boolean remove(Object var1) {
LinkedList.Node var2;
if (var1 == null) {
for(var2 = this.first; var2 != null; var2 = var2.next) {
if (var2.item == null) {
this.unlink(var2);
return true;
}
}
} else {
for(var2 = this.first; var2 != null; var2 = var2.next) {
if (var1.equals(var2.item)) {
this.unlink(var2);
return true;
}
}
}
return false;
}
查詢
//這個是挨個從頭開始遍歷,獲取第一次出現的位置
public int indexOf(Object var1) {
int var2 = 0;
LinkedList.Node var3;
if (var1 == null) {
for(var3 = this.first; var3 != null; var3 = var3.next) {
if (var3.item == null) {
return var2;
}
++var2;
}
} else {
for(var3 = this.first; var3 != null; var3 = var3.next) {
if (var1.equals(var3.item)) {
return var2;
}
++var2;
}
}
return -1;
}
//倒著遍歷,然後找到出現的最後一次的位置
public int lastIndexOf(Object var1) {
int var2 = this.size;
LinkedList.Node var3;
if (var1 == null) {
for(var3 = this.last; var3 != null; var3 = var3.prev) {
--var2;
if (var3.item == null) {
return var2;
}
}
} else {
for(var3 = this.last; var3 != null; var3 = var3.prev) {
--var2;
if (var1.equals(var3.item)) {
return var2;
}
}
}
return -1;
}
toArray
public Object[] toArray() {
//又建立一個新的陣列
Object[] var1 = new Object[this.size];
int var2 = 0;
//挨個遍歷,,,然後把美國元素fang放到新的數組裡面233
for(LinkedList.Node var3 = this.first; var3 != null; var3 = var3.next) {
var1[var2++] = var3.item;
}
return var1;
}
回答問題
問: LinkedList的底層用什麼實現的?
答:LinkedList的底層是用雙向連結串列來實現的
問:LinkedList的插入刪除的時候的高效如何實現?
答:首先是因為LinkedList的底層是用雙向連結串列實現的,所以插入和刪除比較高效,不需要大量的資料移動,其次,在獲取結點的時候,核心的方法是node()方法,採用的思想是折半查詢。判斷index是在前半區間還是後半區間,如果在前半區間就從head搜尋,而在後半區間就從tail搜尋。而不是一直從頭到尾的搜尋,將將節點訪問的複雜度由O(n)變為O(n/2)。
問:LinkedList執行緒安全嗎?為什麼?
答:執行緒不安全
問:LinkedList和ArrayList的比較
答:1 對ArrayList和LinkedList而言,在列表末尾增加一個元素所花的開銷都是固定的。對ArrayList而言,主要是在內部陣列中增加一項,指向所新增的元素,偶爾可能會導致對陣列重新進行分配;而對LinkedList而言,這個開銷是統一的,分配一個內部Entry物件。
2在ArrayList的中間插入或刪除一個元素意味著這個列表中剩餘的元素都會被移動;而在LinkedList的中間插入或刪除一個元素的開銷是固定的。
3當進行頻繁的插入刪除的操作的時候,LinkedList的效能會更加好一點。
問:ArrayList,LinkedList與ArrayList的不同
答:第一點:List是介面類,LinkedList和ArrayList是實現類
第二點:ArrayList是動態陣列(順序表)的資料結構。順序表的儲存地址是連續的,所以在查詢比較快,但是在插入和刪除時,由於需要把其它的元素順序向後移動,耗時操作。
第三點:LinkedList是連結串列的資料結構。連結串列的儲存地址是不連續的,每個儲存地址通過指標指向,在查詢時需要進行通過指標遍歷元素,所以在查詢時比較慢。由於連結串列插入時不需移動其它元素,所以在插入和刪除時比較快。