List 表示的就是線性表,是具有相同特性的數據元素的有限序列。它主要有兩種存儲結構,順序存儲和鏈式存儲,分別對應着 ArrayList 和 LinkedList 的實現,接下來以 jdk7 代碼爲例,對這兩種實現的核心源碼進行分析。
1. ArrayList 源碼分析
ArrayList 是基於數組實現的可變大小的集合,底層是一個 Object[] 數組,可存儲包括 null 在內的所有元素,默認容量爲 10。元素的新增和刪除,本質就是數組元素的移動。
1.1 add 操作
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
}
1.2 remove 操作
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;
}
1.3 遍歷
常見的遍歷代碼如下:
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();
}
}
2. LinkedList 源碼分析
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;
}
}
它包含三個成員變量:
- int size = 0: 結點總數
- Node
first: 頭結點 - Node
last: 尾結點
2.1 頭插法和尾插法
鏈表插入時有兩種插入方法,頭插法和尾插法,關鍵操作就是正確的斷鏈和續鏈。頭插法的代碼如下:
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
}
2.2 刪除節點
同樣的刪除也是分爲從頭或尾刪除,頭節點的刪除,就是讓 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;
}
2.3 棧和隊列
棧就是只能在棧頂操作,後進先出的受限線性表,LinkedList 提供的與棧相關的方法有:
- push(E e): 將元素插入棧頂,也就是插到列表的頭,實際調用的是 linkFirst
- pop(): 從棧頂彈出一個元素,也就是刪除並返回列表的第一個元素,實際調用的是 unlinkFirst
- peek(): 查看棧頂元素,不會刪除
- peekFirst(),peekLast(): 分別是查看棧頂和棧尾元素
隊列就是隻能從一端插入,另一端刪除的線性表,LinkedList 提供的與隊列相關的方法:
- offer(E e): 入隊列,將元素插入列表尾部,實際調用的是 linkLast
- offerFirst,offerLast: 分別是從頭開始或從尾開始入隊,當然了,確認在哪端就不要更改了
- remove(): 出隊列,將列表頭結點刪除並返回
- removeFirst,removeLast: 與 offerFirst,offerLast 搭配使用
2.4 遍歷
雙鏈表有兩種遍歷方式:順序遍歷和逆序遍歷,分別通過 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();
}
}
3. 非線程安全
ArrayList 和 LinkedList 都是非線程安全的,那爲什麼呢?
ArrayList 本質操作可分爲以下兩類,這也是線程競爭條件的所在:
- 對 size 變量的操作,size++ 和 --size
- System.arraycopy() 數組拷貝
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
4. 性能
ArrayList 具備數組隨機訪問的特性,但增加和刪除需要移動數組元素,效率較慢。在動態擴容時,涉及到了內存拷貝,所以適當增加初始容量或者在添加大量數據之前提前擴大容量,減少拷貝次數是有必要的。
相比 ArrayList,LinkedList 只能順序或逆序訪問,佔用的內存稍微大點,因爲節點還要維護兩個前後引用,但是它的插入刪除效率高。
5. 小結
這兩個是很常用的數據結構,也比較容易理解,在閱讀源碼時,jdk裏類的設計,編碼方式也值得去重視。