ArrayList 和 LinkedList 源碼分析

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) 方法可以把 ArrayList 和 LinkedList 變成線程安全的。

4. 性能

ArrayList 具備數組隨機訪問的特性,但增加和刪除需要移動數組元素,效率較慢。在動態擴容時,涉及到了內存拷貝,所以適當增加初始容量或者在添加大量數據之前提前擴大容量,減少拷貝次數是有必要的。

相比 ArrayList,LinkedList 只能順序或逆序訪問,佔用的內存稍微大點,因爲節點還要維護兩個前後引用,但是它的插入刪除效率高。

5. 小結

這兩個是很常用的數據結構,也比較容易理解,在閱讀源碼時,jdk裏類的設計,編碼方式也值得去重視。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章