List 表示的就是線性表,是具備相同特性的數據元素的有限序列。它主要有兩種存儲結構,順序存儲和鏈式存儲,分別對應着 ArrayList 和 LinkedList 的實現,接下來以 jdk7 代碼爲例,對這兩種實現的核心源碼進行分析。java
ArrayList 是基於數組實現的可變大小的集合,底層是一個 Object[] 數組,可存儲包括 null 在內的全部元素,默認容量爲 10。元素的新增和刪除,本質就是數組元素的移動。node
ArrayList 內部有一個 size 成員變量,記錄集合內元素總數,add 操做的本質就是 elementData[size++] = e,爲了保證插入成功,會按需對數組進行擴容,擴容代碼以下:數組
private void grow(int minCapacity) {
// 有可能會溢出
int oldCapacity = elementData.length;
// 至關於 oldCapacity+(oldCapacity/2),擴大 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 確保新容量不小於 minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 檢查擴充的最小容量是否溢出,若是溢出值會小於 0
newCapacity = hugeCapacity(minCapacity);
// 生成一個新數組,舊數組沒有被引用會被垃圾回收
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
add 操做還有一個指定位置的插入,來看具體實現(本文首發於微信公衆號:頓悟源碼,qq交流羣:673986158):緩存
public void add(int index, E element) {
// 檢查下標是否有效
// 可能會拋出 IndexOutOfBoundsException 異常
rangeCheckForAdd(index);
// 確保足夠的容量插入成功
ensureCapacityInternal(size + 1); // Increments modCount!!
// 把從 index 位置開始的全部元素後移一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 插入新元素
elementData[index] = element;
size++; // 總數加 1
}
複製代碼
remove 分爲兩種,按下標刪除和按元素刪除。按元素首先會遍歷找到匹配元素的位置下標,而後按下標進行刪除:安全
public E remove(int index) {
// 檢查下標位置是否有效
rangeCheck(index);
// 更新列表結構的修改次數
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 將 index 後的全部元素前移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 釋放引用
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
複製代碼
常見的遍歷代碼以下:微信
for (int i=0; i<list.size(); i++) {
list.get(i); // do something
}
複製代碼
這種遍歷方式的缺點是,在遍歷過程當中若是修改集合結構(好比調用 remove 或 add),沒有任何異常,可能會致使意想不到的輸出,另一種遍歷方式,好比:數據結構
for (T obj:list) {
// do something
}
複製代碼
遍歷的過程當中,若是使用不正當的刪除操做(好比list.remove)就會拋出 ConcurrentModificationException,由於它默認使用的 Iterator 遍歷方式。併發
Iterator 是 jdk 爲全部集合遍歷而設計,而且是 fail-fast 快速失敗的。在 iterator 遍歷過程當中,若是 List 結構不是經過迭代器自己的 add/remove 方法而改變,那麼就會拋出 ConcurrentModificationException。注意,在不一樣步修改的狀況下,它不能保證會發生,它只是盡力檢測併發修改的錯誤。源碼分析
fail-fast 是經過一個 modCount 字段來實現的,這個字段記錄了列表結構的修改次數,當調用 iterator() 返回迭代器時,會緩存 modCount 當前的值,若是這個值發生了不指望的變化,那麼就會在 next, remove 操做中拋出異常,核心代碼以下:性能
private class Itr implements Iterator<E> {
int cursor; // 下一個要返回的元素下標,初始爲 0
// 最後一個返回的元素下標,-1 表示沒有元素
int lastRet = -1;
// 緩存 modCount 的值
int expectedModCount = modCount;
// 是否還有元素可讀
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 檢查 modCount 值是否變化
checkForComodification();
int i = cursor; // 元素下標
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 調用 iterator 自身的 remove
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 刪除元素,此時 modCount 值改變
ArrayList.this.remove(lastRet);
// 移除會把lastRet後面的元素前移一位
cursor = lastRet; // 因此仍是從 lastRet 開始讀
lastRet = -1;
// 重置 modCount 指望值
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
// 若是變化,檢測到併發修改,拋出異常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
複製代碼
LinkedList 底層是一個雙向鏈表,它不只實現了 List 接口,還實現了 Deque 接口,因此既能夠把它看成通常線性表,也可看成受限線性表主要是棧和隊列。
雙向鏈表的每一個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。因此,從雙向鏈表中的任意一個結點開始,均可以很方便地訪問它的前驅結點和後繼結點,LinkedList 中的節點定義以下:
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;
}
}
複製代碼
它包含三個成員變量:
鏈表插入時有兩種插入方法,頭插法和尾插法,關鍵操做就是正確的斷鏈和續鏈。頭插法的代碼以下:
private void linkFirst(E e) {
// 使用臨時變量指向頭節點
final Node<E> f = first;
// 新節點前驅爲 null,後繼爲 first
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null) // 鏈表爲空,同時爲最後一個節點
last = newNode;
else // 不然做爲前驅節點
f.prev = newNode;
size++;
modCount++;
}
複製代碼
頭插法的結果是逆序的,尾插法的結果是順序的。尾插法的操做也比較簡單,直接修改 last 引用便可:
void linkLast(E e) {
// 使用臨時變量指向尾節點
final Node<E> l = last;
// 新節點前驅爲 last,後繼爲 null
final Node<E> newNode = new Node<>(l, e, null);
// 重置 last 指向節點
last = newNode;
if (l == null)// 鏈表爲空
first = newNode; // 第一個節點
else // 不然做爲前一個的後繼
l.next = newNode;
size++; // 更新列表總數和結構修改次數
modCount++;
}
複製代碼
頭插和尾插都只修改了一個引用,比較複雜的是在中間某個位置插入,其原理和代碼以下:
// 在非null節點succ以前插入元素e
void linkBefore(E e, Node<E> succ) {
// assert succ != null; 如下代碼順序不能變
// 記住 succ 的前驅節點
final Node<E> pred = succ.prev;
// 1-2 建立一個新節點,它的前驅指向 pred,後繼指向 succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 3 succ 前驅指向新節點
succ.prev = newNode;
if (pred == null) // 若是是第一個節點
first = newNode; // 讓first也指向新節點
else // 4 不然做爲 pred 的後繼節點
pred.next = newNode;
size++; // 元素總數加1
modCount++; // 列表結構修改次數加1
}
複製代碼
一樣的刪除也是分爲從頭或尾刪除,頭節點的刪除,就是讓 first 指向它的後繼節點,代碼以下:
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 爲了返回元素的數據
final E element = f.item;
// 臨時變量引用頭節點的後繼節點
final Node<E> next = f.next;
// 釋放頭節點,所有至 null
f.item = null;
f.next = null; // help GC
// 重置 first 引用
first = next;
// 若是刪除的鏈表是最後一個節點
if (next == null)
last = null;
else // 頭節點的前驅爲 null
next.prev = null;
size--;
modCount++;
return element;
}
複製代碼
尾節點的刪除,同樣是修改 last 引用,讓它指向它的前驅節點,與頭節點刪除邏輯差很少,可對照理解,代碼以下:
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; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
複製代碼
若是在某個中間位置刪除,就須要正確的操做了,主要是防止在斷鏈的過程當中致使整個鏈條斷開,代碼以下:
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 {
// 1 前驅節點的後繼指向 x 的後繼節點
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
// 2 後繼節點的前驅指向 x 的前驅節點
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
複製代碼
棧就是隻能在棧頂操做,後進先出的受限線性表,LinkedList 提供的與棧相關的方法有:
隊列就是隻能從一端插入,另外一端刪除的線性表,LinkedList 提供的與隊列相關的方法:
雙鏈表有兩種遍歷方式:順序遍歷和逆序遍歷,分別經過 ListItr 和 DescendingIterator 實現。一樣這個 Iterator 也是快速失敗的,其中 ListItr 分別提供了向前和向後的遍歷方式,DescendingIterator 只是簡單對 ListItr previous() 方法使用的封裝,ListItr 核心代碼以下:
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned = null;// 最後返回的節點
private Node<E> next;// 下一個要讀的節點
private int nextIndex;// 下一個要讀的節點位置
// 緩存列表結構修改次數,檢查併發修改
private int expectedModCount = modCount;
// 初始化開始遍歷的位置,node(index) 會遍歷找到這個節點
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
// 順序讀時,判斷是否還有更多的後繼節點
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next; // 移動 next 指向其後繼節點
nextIndex++;
return lastReturned.item;
}
// 逆序讀時,判斷是否還有更多的前驅節點
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// 移動 next 指向其前驅節點
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public void remove() {
// 遍歷過程當中,提供刪除操做
}
public void add(E e) {
// 遍歷過程當中,提供新增操做
}
// 檢查是否存在併發修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
複製代碼
ArrayList 和 LinkedList 都是非線程安全的,那爲何呢?
ArrayList 本質操做可分爲如下兩類,這也是線程競爭條件的所在:
size++ 實際上是一個複合操做:取值、加1和賦值,不是原子操做,非線程安全;System.arraycopy 它也是非線程安全的,因此 ArrayList 不是線程安全的。
LinkedList 主要競爭條件就是斷鏈和續鏈的操做,以尾插爲例,假如線程 A 執行:
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
複製代碼
若是如今線程 A 被搶佔,線程 B 也執行相同的代碼,而且繼續執行:
last = newNode;
複製代碼
不久後,線程 A 也執行上述代碼,那麼問題就出來了,線程 B 完成操做後,線程 A 就操做了一次,致使線程 B 的要插入的節點丟失,因此不是線程安全的。
固然了 JDK 提供了 Collections.synchronizedList(List) 方法能夠把 ArrayList 和 LinkedList 變成線程安全的。
ArrayList 具有數組隨機訪問的特性,但增長和刪除須要移動數組元素,效率較慢。在動態擴容時,涉及到了內存拷貝,因此適當增長初始容量或者在添加大量數據以前提早擴大容量,減小拷貝次數是有必要的。
相比 ArrayList,LinkedList 只能順序或逆序訪問,佔用的內存稍微大點,由於節點還要維護兩個先後引用,可是它的插入刪除效率高。
這兩個是很經常使用的數據結構,也比較容易理解,在閱讀源碼時,jdk裏類的設計,編碼方式也值得去重視。