1、前言java
在分析了ArrayList了以後,緊接着必需要分析它的同胞兄弟:LinkedList,LinkedList與ArrayList在底層的實現上有所不一樣,其實,只要咱們有數據結構的基礎,在分析源碼的時候就會很簡單,下面進入正題,LinkedList源碼分析。node
2、LinkedList數據結構數組
仍是老規矩,先抓住LinkedList的核心部分:數據結構,其數據結構以下數據結構
說明:如上圖所示,LinkedList底層使用的雙向鏈表結構,有一個頭結點和一個尾結點,雙向鏈表意味着咱們能夠從頭開始正向遍歷,或者是從尾開始逆向遍歷,而且能夠針對頭部和尾部進行相應的操做。多線程
3、LinkedList源碼分析ide
3.1 類的繼承關係 函數
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
說明:LinkedList的類繼承結構頗有意思,咱們着重要看是Deque接口,Deque接口表示是一個雙端隊列,那麼也意味着LinkedList是雙端隊列的一種實現,因此,基於雙端隊列的操做在LinkedList中所有有效。源碼分析
3.2 類的內部類 優化
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就是實際的結點,用於存放實際元素的地方。this
3.3 類的屬性
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { // 實際元素個數 transient int size = 0; // 頭結點 transient Node<E> first; // 尾結點 transient Node<E> last; }
說明:LinkedList的屬性很是簡單,一個頭結點、一個尾結點、一個表示鏈表中實際元素個數的變量。注意,頭結點、尾結點都有transient關鍵字修飾,這也意味着在序列化時該域是不會序列化的。
3.4 類的構造函數
1. LinkedList()型構造函數
public LinkedList() { }
2. LinkedList(Collection<? extends E>)型構造函數
public LinkedList(Collection<? extends E> c) { // 調用無參構造函數 this(); // 添加集合中全部的元素 addAll(c); }
說明:會調用無參構造函數,而且會把集合中全部的元素添加到LinkedList中。
3.5 核心函數分析
1. add函數
public boolean add(E e) { // 添加到末尾 linkLast(e); return true; }
說明:add函數用於向LinkedList中添加一個元素,而且添加到鏈表尾部。具體添加到尾部的邏輯是由linkLast函數完成的。
void linkLast(E e) { // 保存尾結點,l爲final類型,不可更改 final Node<E> l = last; // 新生成結點的前驅爲l,後繼爲null final Node<E> newNode = new Node<>(l, e, null); // 從新賦值尾結點 last = newNode; if (l == null) // 尾結點爲空 first = newNode; // 賦值頭結點 else // 尾結點不爲空 l.next = newNode; // 尾結點的後繼爲新生成的結點 // 大小加1 size++; // 結構性修改加1 modCount++; }
說明:對於添加一個元素至鏈表中會調用add方法 -> linkLast方法。
對於添加元素的狀況咱們使用以下示例進行說明
示例一代碼以下(只展現了核心代碼)
List<Integer> lists = new LinkedList<Integer>(); lists.add(5); lists.add(6);
說明:首先調用無參構造函數,以後添加元素5,以後再添加元素6。具體的示意圖以下:
說明:上圖的代表了在執行每一條語句後,鏈表對應的狀態。
2. addAll函數
addAll有兩個重載函數,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,咱們平時習慣調用的addAll(Collection<? extends E>)型會轉化爲addAll(int, Collection<? extends E>)型,因此咱們着重分析此函數便可。
// 添加一個集合 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) { // 若是插入位置爲鏈表末尾,則後繼爲null,前驅爲尾結點 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) // 表示在第一個元素以前插入(索引爲0的結點) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { // 表示在最後一個元素以後插入 last = pred; } else { pred.next = succ; succ.prev = pred; } // 修改實際元素個數 size += numNew; // 結構性修改加1 modCount++; return true; }
說明:參數中的index表示在索引下標爲index的結點(其實是第index + 1個結點)的前面插入。在addAll函數中,addAll函數中還會調用到node函數,get函數也會調用到node函數,此函數是根據索引下標找到該結點並返回,具體代碼以下
Node<E> node(int 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; // 返回該結點 } }
說明:在根據索引查找結點時,會有一個小優化,結點在前半段則從頭開始遍歷,在後半段則從尾開始遍歷,這樣就保證了只須要遍歷最多一半結點就能夠找到指定索引的結點。
下面經過示例來更深刻了解調用addAll函數後的鏈表狀態。
List<Integer> lists = new LinkedList<Integer>(); lists.add(5); lists.addAll(0, Arrays.asList(2, 3, 4, 5));
上述代碼內部的鏈表結構以下:
3. unlink函數
在調用remove移除結點時,會調用到unlink函數,unlink函數具體以下:
E unlink(Node<E> x) { // 保存結點的元素 final E element = x.item; // 保存x的後繼 final Node<E> next = x.next; // 保存x的前驅 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--; // 結構性修改加1 modCount++; // 返回結點的舊元素 return element; }
說明:將指定的結點從鏈表中斷開,再也不累贅。
4、針對LinkedList的思考
1. 對addAll函數的思考
在addAll函數中,傳入一個集合參數和插入位置,而後將集合轉化爲數組,而後再遍歷數組,挨個添加數組的元素,可是問題來了,爲何要先轉化爲數組再進行遍歷,而不是直接遍歷集合呢?從效果上二者是徹底等價的,均可以達到遍歷的效果。關於爲何要轉化爲數組的問題,個人思考以下:1. 若是直接遍歷集合的話,那麼在遍歷過程當中須要插入元素,在堆上分配內存空間,修改指針域,這個過程當中就會一直佔用着這個集合,考慮正確同步的話,其餘線程只能一直等待。2. 若是轉化爲數組,只須要遍歷集合,而遍歷集合過程當中不須要額外的操做,因此佔用的時間相對是較短的,這樣就利於其餘線程儘快的使用這個集合。說白了,就是有利於提升多線程訪問該集合的效率,儘量短期的阻塞。
5、總結
分析完了LinkedList源碼,其實很簡單,值得注意的是LinkedList能夠做爲雙端隊列使用,這也是隊列結構在Java中一種實現,當須要使用隊列結構時,能夠考慮LinkedList。謝謝各位園友觀看~