本篇文章主要分析一下 Java 集合框架中的 List 部分,LinkedList,該源碼分析基於JDK1.8,分析工具,AndroidStudio,文章分析不足之處,還請指正!java
首先,咱們來看下 LinkedList 的類繼承結構:node
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
複製代碼
從上面咱們能夠看出來 LinkedList 是一個繼承於 AbstractSequentialList 的雙向鏈表。它也能夠被看成堆棧、隊列或雙端隊列進行操做。 實現 List 接口,能對它進行隊列操做。實現 Deque 接口,即能將 LinkedList 看成雙端隊列使用。實現了 Cloneable 接口,即覆蓋了函數clone(),能克隆。實現 java.io.Serializable 接口,這意味着 LinkedList 支持序列化,能經過序列化去傳輸。同時 LinkedList 是非同步的。數組
接下來咱們來看看LinkedList的數據結構是什麼樣的。咱們在分析 LinkedHashMap 的時候也會發現它的底層包含一個雙向循環鏈表,而 LinkedList 也是的,LinkedList 底層的數據結構是基於雙向循環鏈表的,且頭結點中不存放數據,以下: bash
圖上即是 LinkedList 的底層結構圖,基於雙向循環鏈表結構設計的。它的每個節點都包含數據信息,上一個節點位置信息和下一個節點位置信息。數據結構
接下來咱們分析 LinkedList 的源碼,首先來看下它內部申明的屬性。框架
//鏈表節點的數量
transient int size = 0;
//頭結點
transient Node<E> first;
//下一個節點
transient Node<E> last;
複製代碼
變量申明比較簡單,接着咱們分析構造函數。若是有不大理解的,咱們能夠先分析後面的代碼,就能知道這些屬性的意思了。函數
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
複製代碼
構造函數也比較簡單,沒啥可說的,咱們接着分析。一開始介紹的時候咱們說明了底層是雙向循環鏈表,因此確定有節點信息,因此咱們來看下它的節點數據結構。工具
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;
}
}
複製代碼
節點數據結構,很簡單,一個是數據信息 item,一個是前一個位置節點 prev,另外一個是後一個位置節點 next,很是簡單。接下來咱們繼續查看添加元素 add 方法源碼分析
public boolean add(E e) {
linkLast(e);
return true;
}
複製代碼
添加數據 add 方法比較簡單,add 函數用於向 LinkedList 中添加一個元素,而且添加到鏈表尾部。咱們來看看 linkLast 方法怎麼添加數據的。學習
//尾部添加數據e
void linkLast(E e) {
//保存尾部節點
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++;
modCount++;
}
複製代碼
上面這段代碼是什麼意思呢?咱們來經過 demo 來簡單說明一下。
List<String> mLinkedList = new LinkedList();
mLinkedList.add("A");
mLinkedList.add("B");
複製代碼
首先咱們先申明一個 LinkedList 類型的變量 mLinkedList,而後依次添加進兩個數據 「A」 和 「B」,這個時候,它的內部結構是怎麼操做的呢?
咱們首先調用 LinkedList 的無參構造函數,進行初始化,這個時候裏面的節點都是 null,由於沒有數據,其次,咱們經過 add 添加進一條數據 「A」,這個時候結合上面的源代碼 add 方法,來解讀一下,首先會調用 linkLast 方法,其次判斷它的 last 節點是否爲null,因爲是剛初始化的,因此 last=null,這樣就會調用 first = newNode;這個方法,也就是在開頭節點處插入一條數據,其次再調用 add(「B」),再次插入一條數據,咱們循環上面那段代碼,發現這個時候 last!=null 了,因此調用 last.next=newNode 代碼,也就是在第一個節點的後面插入一條數據。
咱們接着分析 add 其餘的重載方法。
//在指定的位置上面插入一條數據
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
複製代碼
這個方法也不是很複雜,咱們依次分析,首先判斷一下,須要插入的 index 是否越界。在插入以前咱們須要先找到待插入位置的 node,經過 node(index) 方法得到,咱們看下
//判斷index是在鏈表的中部以前仍是後面,若是在 1/2 前的話,從頭開始遍歷,若是在 1/2 後的話,從後往前遍歷
Node<E> node(int index) {
//從前日後遍歷獲取index出的node
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//從後往前遍歷獲取index出的node
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
複製代碼
上面很簡單,爲了提升搜索效率,先判斷是在 1/2 處的前面仍是後面,而後依次循環得到 index處的 node,咱們接着分析
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//不在[0,size]範圍內越界
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
複製代碼
其次根據 index 判斷是在鏈表中的哪一個地方插入數據,是尾部仍是在前面位置,若是是在尾部的話,咱們已經分析過了,咱們來看下在前面插入數據的狀況。
void linkBefore(E e, Node<E> succ) {
//保存目標index處的前置節點
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++;
}
複製代碼
咱們逐一分析,首先咱們獲取到了 index 處的節點 C,而後保存 C 的前置節點爲 pred (也就是 B 節點),而後咱們用待插入的節點構建一個新的節點 (newNode)(新節點的前置節點是 pred,後繼節點是 C),而後咱們將 index 處的節點 C 的前置節點設置爲待插入的節點,而後判斷這個 index 處的 node 是不是第一個節點,不是的話就將原先 index 處的 node 的前置節點的後繼節點設置爲待插入的節點。這裏面主要涉及到鏈表的插入操做,若是對於這不熟悉的話,能夠先去複習一下鏈表的相關操做。
分析完了 add 方法,咱們再來分析一下 addAll 方法。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
複製代碼
很簡單,在鏈表尾部插入一個集合,咱們看一下 addAll 的重載方法,仍是蠻長的,咱們逐行看一下:
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) {
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) {
last = pred;
} else {
//中間保存後面的鏈表數據
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
複製代碼
以上即是 add 的方法內容,思想其實也簡單的,相似單個插入,主要就是判斷在鏈表尾部仍是中間或者頭部插入,而後再依次插入便可。
插入數據分析完以後,咱們就開始分析獲取數據 get。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
複製代碼
get 方法也是隻有簡單的兩行,咱們依次分析,首先分析第一行,就是判斷索引的位置是否越界,
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
複製代碼
很簡單,看代碼就知道了,不用分析了,接着分析第二行,第二行上面也已經分析過了,因此這裏就不用再多敘述了。get 方法整體而言很簡單。咱們接着分析。
咱們接着來分析一下刪除數據的代碼。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
複製代碼
代碼也很簡單,首先是檢查索引是否越界,其次咱們分析第二行,咱們發現 node(index) 的意思就是獲取鏈表中指定位置上面的 node,因此咱們看看 unlink 方法內容。
E unlink(Node<E> x) {
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;
}
//gc
x.item = null;
size--;
modCount++;
return element;
}
複製代碼
上面主要是進行雙向鏈表的刪除操做。咱們接着分析它的重載方法,
//刪除指定元素,默認從first節點開始,刪除第一次出現的那個元素
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;
}
複製代碼
相應代碼已經分析過了,咱們就再也不說了。咱們分析一下 set 方法
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
複製代碼
首先是判斷是否越界,而後獲取須要替換的位置上面的 node,而後 update 一下,返回舊的數據。
咱們來分析一下清空鏈表的方法 clear
public void clear() {
//遍歷鏈表,依次設置爲null
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
複製代碼
基本上,LinkedList 的方法分析完了,還有一些簡單的方法,咱們一併列出來,作個簡單的註釋。
在首節點處添加一個數據
//在首節點處添加一個數據
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
public void addFirst(E e) {
linkFirst(e);
}
複製代碼
在尾節點插入一個節點
//在尾節點插入一個節點
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
public void addLast(E e) {
linkLast(e);
}
複製代碼
移除第一個節點
//移除第一個節點
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; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
複製代碼
移除並返回最後一個節點
移除並返回最後一個節點
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
複製代碼
返回第一個節點
//返回第一個節點
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
複製代碼
返回最後一個節點
//返回最後一個節點
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
複製代碼
是否包含某個節點信息
//是否包含某個節點信息
public boolean contains(Object o) {
return indexOf(o) != -1;
}
複製代碼
遍歷鏈表,存在則返回索引不存在則返回-1
//遍歷鏈表,存在則返回索引不存在則返回-1
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
複製代碼
從後往前遍歷,第一次出現則返回索引,不存在返回-1
//從後往前遍歷,第一次出現則返回索引,不存在返回-1
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
複製代碼
返回鏈表第一個節點,可是不刪除節點
//返回鏈表第一個節點,可是不刪除節點
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
複製代碼
返回並刪除第一個節點
//返回並刪除第一個節點
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
複製代碼
//省略迭代部分的源碼......
以上即是 LinkedList 的基本源碼,咱們基本都給出了註釋,相關重要的功能都加以圖解,接下來咱們來總結一下關於 LinkedList 相關重要知識。
從源碼中能夠看出,LinkedList 的實現是基於雙向循環鏈表的。卻別與 ArrayList 的數組,以及 HashMap 的線性表和散列表以及 LinkedHashMap 的線性表和散列表以及雙向循環鏈表。 咱們在搜索鏈表中的數據時,都會進行判斷是否爲 null 的狀況,因此 LinkedList 是容許元素爲 null 的狀況的。
LinkedList 是基於鏈表實現的,所以不存在容量不足的問題,因此這裏沒有擴容的方法。 源碼中還實現了棧和隊列的操做方法,所以也能夠做爲棧、隊列和雙端隊列來使用。 LinkedList 是一個繼承於 AbstractSequentialList 的雙向鏈表。它也能夠被看成堆棧、隊列或雙端隊列進行操做。 實現 List 接口,能對它進行隊列操做。實現 Deque 接口,即能將 LinkedList 看成雙端隊列使用。實現了 Cloneable 接口,即覆蓋了函數 clone(),能克隆。實現 java.io.Serializable 接口,這意味着 LinkedList 支持序列化,能經過序列化去傳輸。同時LinkedList 是非同步的。
專一於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎你們關注,一塊兒交流學習~