ArrayList繼承關係分析

王心森發表於2020-08-06

最近翻看ArrayList的原始碼,對ArrayList的繼承關係做了大概梳理,記錄如下!

繼承關係

為了更全面瞭解ArrayList,我們需要首先對ArrayList的繼承關係有個大概瞭解,ArrayList的UML圖譜如下:

拓撲圖

下面,我們根據UML圖譜,自上而下逐個做個簡要介紹,便於ArrayList的理解!

Iterable

這是一個支援for-each迴圈的介面,一共有三個方法:

Iterable

1.iterator():可以獲得一個Iterator物件。Iterator我們都很熟悉了,它可以根據hasNext()next()兩個方法進行迴圈迭代。

2.forEach():這是一個default方法,預設接收Consumer物件,通過for迴圈進行單物件操作。

3.spliterator():這也是一個default方法,可以得到一個Spliterator物件,支援物件的併發操作。後面我們會對Spliterator進行專門的講解!

Collection

它是集合操作的"root interface",每個Collection就代表不同的集合型別,例如:重複集合和非重複集合、有序集合和無序集合等。JDK中沒有對它進行直接的實現,只是提供了實現它的子介面,例如:SetList等。

Collection

List

這是我們最熟悉的有序集合介面,它的實現類,ArrayList、LinkedList是我們最常用。它內部的方法也一目瞭然,不做過多介紹。

ArrayList繼承關係分析

AbstractCollection

這個抽象類是對Collection介面的一個基本實現。

如果實現一個不可變集合,我們只需要繼承這個類,並實現它的iterator()size()方法,當然,iterator()方法返回的iterator物件中的hasNext()next()方法也需要實現。

如果實現一個可變集合,專案中就必須override它的add()方法,預設該方法丟擲異常UnsupportedOperationException,同時,實現iterator()方法返回的iterator物件中的hasNext()next()以及remove()方法。

AbstractCollection

AbstractList

此類提供了List介面的基本實現,以最小化實現由“隨機訪問”資料儲存(例如陣列)支援的該介面所需的工作量。

實現不可變List集合時,繼承該類,並實現其中的get()size()方法。

實現可變List集合時,還需要實現其中的set()方法,如果集合是可變長度集合,override其中的add()remove()方法。

AbstractList

** 屬性modCount:**

AbstractList中有個重要屬性modCount,int型別,一旦類內部結構被修改,該屬性就會進行累加,,用來表示該類被修改的次數。在AbstractList的子類中,會存在與modCount向對應的另一個屬性expectedModCount,子類可以通過對比兩個值是否相等,來達到校驗該類是否被其他執行緒修改的目的,該功能稱為fast-fail。例如:AbstractList中add()方法都會對modCount進行累加操作,如果一個執行緒A在對它進行遍歷add操作時,另一執行緒B也對它進行了add操作,那麼A執行緒就會檢測到expectedModCountmodCount不一致,從而丟擲異常ConcurrentModificationException

RandomAccess

這只是一個介面,內部沒有方法,它僅作為一個標記介面存在,表示List集合下的實現支援快速隨機訪問功能,簡單來說就是底層是陣列實現的集合標識。

Serializable

序列化介面,至於介面為什麼需要序列化,可以參考:物件序列化為何要定義serialVersionUID的來龍去脈.

Cloneable

實現Cloneable介面的類,可以合法地使用Object的clone()方法對該類例項進行按欄位複製,沒有實現Cloneable介面的類呼叫Object的clone()方法時,則會導致丟擲異常CloneNotSupporteddException

這裡涉及到淺克隆(shallow clone)和深克隆(deep clone)的知識,在此不做過多介紹!可參考:物件的深度複製和淺複製.

Itr

AbstractList與ArrayList內部都有一個內部類Itr,都實現了Iterator介面的hasNext()next()方法,並將預設remove()方法重寫,另外,ArrayList中還重寫了forEachRemaining()方法,該方法是對未處理過的元素進行遍歷。

Itr對元素遍歷過程使用到了三個重要屬性:cursor、lastRet、expectedModCount:

cursor:表示下一個返回元素的指標;
lastRet:最後一次返回的元素指標,沒有的話,預設-1;
expectedModCount:初始化迭代器時,預設為AbstractList中的modCount值,迭代過程中,會與modCount進行比較,達到"fail-fast"效果,保證執行緒同步安全,不相同時,就會丟擲ConcurrentModificationException異常。

ListItr

同理,AbstractList與ArrayList通過ListItr對迭代器Itr進行繼承,實現了迭代效果,兩則的不同點,ArrayList.ListItr比AbstractList.ListItr更加優化了。

ListItr

另外,ListItr除了繼承Itr外,它還實現了ListIterator。相比於父類Iterator,ListIterator新增瞭如下方法:

hasPrevious():相對於hasNext()方法,判斷是否有前一個元素;
previous():相對於next()方法,返回前一個元素;
nextIndex():下一個元素的index值;
previousIndex():前一個元素的index值;
remove():刪除當前元素;
set(E e):修改當前元素;
add(E e):新增元素;

總的來說,ListIterator使迭代器相容向前、向後兩個方向的遍歷,並能夠對元素進行修改、新增和刪除。

ListIterator

SubList

這是一個支援對ArrayList區域性操作的集合,從構造方法中可以看到,我們操作的是fromIndextoIndexparent物件,offset是迭代操作的偏移量。

SubList(AbstractList<E> parent,
        int offset, int fromIndex, int toIndex) {
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;
}

因為它支援對集合從fromIndextoIndex的區段進行隨機訪問,因此實現了RandomAccess介面,前面我們講過,這是一個隨機訪問標識。另外,我們對區段操作與ArrayList相同,所以繼承ArrayList。

SubList

ArrayListSpliterator

前面我們在Itrable中提到了spliterator()方法,它可以得到一個Spliterator物件,支援併發操作。那麼ArrayList中就對Spliterator進行了具體實現,實現類就是ArrayListSpliterator

ArrayListSpliterator

要想了解ArrayListSpliterator,我們首先來看一下它的三個欄位屬性含義:

// 接收到的list物件
private final ArrayList<E> list;
// 開始位置
private int index; // current index, modified on advance/split
// 結束位置(不包含)
private int fence; // -1 until used; then one past last index
// 期望的ModCount值
private int expectedModCount; // initialized when fence set

其中,我們操作的元素區間就是:[index, fence),即:[index, fence-1]。

ArrayListSpliterator中有三個重要方法:

trySplit():通過"二分法"分隔List集合;
tryAdvance():消費其中單個元素,此時index會相應+1;
forEachRemaining():遍歷所有未消費的集合元素;

具體操作,我們舉例說明,首先我們ArrayListSpliterator新增一個toString()方法,便於列印觀察。

@Override
public String toString() {
      return "[" + this.index + "," + getFence() + ")";
}

1.對trySplit()舉例測試:

public static void main(String[] args) {
  // 初始化集合
  ArrayList<Integer> list = new ArrayList<>();
  for (int i = 0; i <= 10; i++) {
      list.add(i);
  }

  // 建立ArrayListSpliterator物件
  log.info("--------------建立ArrayListSpliterator物件-----------------");
  ArrayListSpliterator<Integer> list_1 = new ArrayListSpliterator<>(list, 0, -1);
  log.info("list_1:" + list_1);    // [0,11)

  // 對list_1進行分割
  log.info("--------------對list_1進行分割-----------------");
  ArrayListSpliterator<Integer> list_2 = list_1.trySplit();
  log.info("list_1:" + list_1);    // [5,11)
  log.info("list_2:" + list_2);    // [0,5)
  // 分割流程:[0,11)(list_1) ---> [0,5)(list_2) + [5,11)(list_1)

  // 對list_1和list_2進行分割
  log.info("--------------對list_1和list_2進行分割-----------------");
  ArrayListSpliterator<Integer> list_3 = list_1.trySplit();
  ArrayListSpliterator<Integer> list_4 = list_2.trySplit();
  log.info("list_1:" + list_1);   // [8,11)
  log.info("list_2:" + list_2);   // [2,5)
  log.info("list_3:" + list_3);   // [5,8)
  log.info("list_4:" + list_4);   // [0,2)
  // 分割流程:
  // [0,5)(list_2)  --> [0,2)(list_4)  + [2,5)(list_2)
  // [5,11)(list_1) --> [8,11)(list_1) + [5,8)(list_3)

  // 測試集合地址
  log.info("--------------測試集合地址-----------------");
  log.info("(list_1.list == list_2.list) = " + (list_1.list == list_2.list));
  log.info("(list_2.list == list_3.list) = " + (list_2.list == list_3.list));
  log.info("(list_3.list == list_4.list) = " + (list_3.list == list_4.list));
}

列印結果:

結果

從結果我們看到,因為Spliterator中都共享一個list,所以他們的list地址都相同,是同一個list物件。

2.對tryAdvance()forEachRemaining()舉例測試:

public static void main(String[] args) {
    // 初始化集合
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i <= 10; i++) {
        list.add(i);
    }
    // tryAdvance操作
    ArrayListSpliterator<Integer> list_1 = new ArrayListSpliterator<>(list, 0, -1);
    list_1.tryAdvance(t -> log.info("tryAdvance:" + t + " "));
    // forEachRemaining操作
    list_1.forEachRemaining(t -> log.info("forEachRemaining:" + t + " "));
    // 剩餘元素
    log.info("list_1:" + list_1);
    log.info("left size:" + list_1.estimateSize());
}

執行結果:

執行結果

從結果我們看到,tryAdvance()只是對第一個元素(index=0)進行了操作,而forEachRemaining()就從第二個元素(index=1)開始對未消費的元素進行遍歷輸出,它的index也隨著增加,最後遍歷完為11,剩餘的size也變為0。

好了,關於ArrayList的繼承關係我們就介紹到這裡,下面開始對ArrayList進行正式分析!

ArrayList分析

ArrayList的資料結構是以陣列為基礎構建的,每個元素都儲存到了陣列當中,ArrayList的size就是此陣列的長度。這裡我針對ArrayList中的主要功能點做個簡要介紹。

ArrayList陣列擴容:

陣列初始化時預設陣列為空物件DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當新增第一個元素時,對陣列進行擴容操作,預設擴容大小為10(DEFAULT_CAPACITY)。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// minCapacity = size + 1
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

擴容操作採用Arrays.copyOf(elementData, newCapacity),其底層是陣列的copy:

System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));

ArrayList中對資料的擴容、縮容或者擷取都是採用此方法操作。它是一個JVM提供的高效陣列拷貝實現,至於它為什麼高效,請參考這篇:System.arraycopy為什麼快

另外,在正常的擴容過程中,陣列容積以1/2的長度進行增長,一直到達Integer.MAX_VALUE - 8。對於Integer.MAX_VALUE - 8的解釋,JDK指出原因是一些VM會在陣列中保留一些header words,導致超出記憶體空間而出現OutOfMemoryError

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList陣列縮容:

陣列縮容操作時(例如刪除),ArrayList中會找到需要移除的index,從index位置開始,將index+1後面的所有元素重新拷貝,同時,最後一位置為null,等待GC回收。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

陣列遍歷:

我們通常使用到的陣列遍歷都是採用的iterator()方法獲取Iterator物件,向後逐個遍歷,其實你也可以採用listIterator()方法獲得一個ListIterator物件實現向前遍歷。

public ListIterator<E> listIterator() {
    return new ListItr(0);
}

public ListIterator<E> listIterator(int index) {
    if (index < 0 || index > size)
      throw new IndexOutOfBoundsException("Index: "+index);
    return new ListItr(index);
}

"Fail-Fast"機制

ArrayList中通過modCoun來實現"Fail-Fast"的錯誤檢測機制,當多個執行緒對同一集合的內容進行操作時,可能就會產生異常。

在ArrayList的迭代器初始化時,會賦值expectedModCount,在迭代過程中判斷modCount和expectedModCount是否一致。比如當A通過iterator去遍歷某集合的過程中,其他執行緒修改了此集合,此時會丟擲ConcurrentModificationException異常。

if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
}         

集合區段操作

通過subList(int fromIndex, int toIndex)方法我們就可以獲得指定索引區段的集合物件SubList。這是一個偏移量控制的集合區段,可以理解為fromIndex-offset變為0後的一個新集合,但要注意,任何對SubList物件的修改操作,都將導致原集合物件修改,因為它們使用的是同一個地址。

public List<E> subList(int fromIndex, int toIndex) {
      subListRangeCheck(fromIndex, toIndex, size);
      return new SubList(this, 0, fromIndex, toIndex);
}

集合分隔操作

當我們需要對集合物件進行多執行緒操作時,可以考慮將集合分隔,採用spliterator()方法獲取到Spliterator物件,利用其trySplit()方法將其分隔開來,分別進行元素操作,分隔方式即為二分法

同樣,我們要注意,Spliterator物件的任何修改,都將導致原集合元素的修改。

public Spliterator<E> spliterator() {
      return new ArrayListSpliterator<>(this, 0, -1, 0);
}

相關文章