Java容器系列-ArrayList源碼分析

ArrayList 是使用的最爲普遍的一個容器。ArrayList 的類的繼承層次圖以下:java

ArrayList 實現了 CollectionList 接口,同時也實現了 CloneableRandomAccess,因此 ArrayList 能夠被拷貝以及具備隨機訪問的特性。數組

本文基於 JDK1.8安全

成員變量

在 ArrayList 類的頭部,定義瞭如下幾個成員變量。微信

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
複製代碼

這幾個變量構成了 ArrayList 的基礎。數據結構

DEFAULT_CAPACITY 表示 ArrayList 的初始容量。elementData 是存儲具體數據的數組,也就是是說,ArrayList 底層數據結構就是一個數組,size 表示 ArrayList 中元素的個數。EMPTY_ELEMENTDATA 表示一個空的 ArrayList 對象,但 ArrayList 中沒有數據時,elementData 指向的就是這個數組對象。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 也表示空的 ArrayList,它只會在實例化一個不帶參數的 ArrayList 的時候被使用一次:多線程

ArrayList list = new ArrayList(); // 此時 elementData 指向的就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
複製代碼

這些變量中須要注意的是 elementData 是不帶訪問修飾符的,這是爲了讓 ArrayList 的內部類能夠方便的訪問它,ArrayList 的內部類後面會講到。elementData 變量是使用 transient 來修飾的,這表示在序列化的時候 elementData 是不會被序列化的,具體的序列化方式後面再講。dom

初始化過程

在上面說到 DEFAULT_CAPACITY 是 ArrayList 的默認容量,值是10,可是須要注意的是,默認容量不必定用的上,在實例化 ArrayList 的時候分三種狀況,第一種不給構造函數傳參,可是此時會新建一個長度爲 10 的對象數組。而是在添加第一個元素的時纔會建立一個長度爲 10 的數組,並把第一個元素添加進去。第二種狀況會給構造參數傳值 n,若是 n 大於0,那麼就會直接建立一個長度爲 n 的對象數組,若是 n 等於 0,那麼就會把 EMPTY_ELEMENTDATA 賦值給 elementData。函數

第三種實例化特殊一點,是直接傳入另外一個容器對象 c 來初始化 ArrayList 對象,此時會先檢查 c 的長度,若是 c 容器裏面沒有元素,直接把 EMPTY_ELEMENTDATA 賦值給 elementData,若是 c 不爲空,就會 c 中的元素拷貝到 elementData 中。post

擴容過程

擴容的過程能夠用如下的流程圖來表示:spa

擴容對 ArrayList 來講是一個很重要的過程,這也是爲何它比數組好用的緣由。

ArrayList 的擴容有兩種方式,一個是自動擴容,一種是手動擴容。自動擴容每次會把當前容器的大小擴大 1.5 倍,手動擴容須要指定大小。既然已經有了自動擴容,那爲何還須要手動擴容呢?設想一個場景,實例化一個 ArrayList 以後,你大概知道會填充一萬個元素,若是這個時候自動擴容的話要通過屢次擴容才能裝下這麼多元素,可是手動指定容器大小的話只須要一次就能夠了。

具體把 ArrayList 擴容到多大是由下面這段代碼決定的:

private int newCapacity(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }
複製代碼

newCapacity 是根據當前的元素的個數計算出來的,右移一位表明除以2,因此 newCapacity 爲當前容量的 1.5 倍。而後這個值會與傳入的值 minCapacity 進行對比,兩個值哪一個大就用哪一個。

爲何每次自動擴容都能爲當前大小的 1.5 倍呢?那是由於自動擴容的時候傳入的 minCapacity 都只比當前的容量大 1,因此確定小於 newCapacity。而 newCapacity 就是 當前容量大小的 1.5 倍。

固然有一個狀況例外,那就是若是在實例化 ArrayList 沒有指定大小的話,ArrayList 會至少擴容到 10。這一機制是靠如下代碼實現的:

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    return Math.max(DEFAULT_CAPACITY, minCapacity);
複製代碼

擴容的時候,都是使用 Arrays.copyOf 將元素拷貝到新的容器中。因此基本都是 O(N) 的時間複雜度,代價很大。因此儘量減小擴容的次數。

注意:ArrayList 沒有縮容的過程。

具體實現

ArrayList 中有了不少的方法,這些方法核心都是圍繞 elementData 操做的。

siez()isEmpty() 方法想着簡單,一個用來返回容器中的元素的數量,一個用來判斷容器是否爲空。

clone()toArray()toArray(T[] a) 這三個方法本質上都是對容器當前的元素作一個備份,都用到了 Arrays.copyOf() 方法。可是須要注意的是 toArray(T[] a) 的實現:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}
複製代碼

在這個方法中除了使用 Arrays.copyOf() 還用到了 System.arraycopy(),其實 Arrays.copyOf() 底層就是使用 System.arraycopy() 方法實現的。可是區別在於前者會返回一個一個新的數組,後者則是直接在原數組上進行操做。

ArrayList 中的 add()get()set()remove() 等方法用於元素的增刪改查,實現並不複雜,只是在操做元素以前須要對容器的 size 進行檢查,若是不知足操做要求,就會報出異常。

euqal() 類的方法主要都是對比每一個元素的類型、順序和值是否一致。

在 JDK1.8 之後,出現了 removeIf() 方法,這個方法使得從容器中刪除元素變得很簡單。

迭代器

ArrayList 中有兩個內部類 ItrListItr,主要方法以下:

private class Itr implements Iterator<E> {
    Itr() {}

    public boolean hasNext() {
    }

    public E next() {
      
    }

    public void remove() {
    }
}
複製代碼
private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
    }

    public int nextIndex() {
    }

    public int previousIndex() {
    }

    public E previous() {
    }

    public void set(E e) {
    }

    public void add(E e) {
    }
}
複製代碼

ListItr 繼承了 Itr,這兩個內部類都實現了迭代器模式,用於遍歷 ArrayList 的元素。從上面的方法可知,Itr 和 ListItr 最大的區別在於 ListItr 能夠從兩個方向對容器的元素進行遍歷。而 Itr 只能使用順着一個方向進行遍歷。

在 JDK1.8 之後,ArrayList 中有一個 ArrayListSpliterator 內部類,這個類用於分割容器。用於提高多線程環境中的處理效率:

ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    list.add(i);
}
Spliterator<Integer> s0 = list.spliterator();
System.out.println(s0.estimateSize()); // 5
Spliterator<Integer> s1 = s0.trySplit();
System.out.println(s1.estimateSize()); // 5
複製代碼

s0 中有 10 個元素,在調用 s0.trySplit() 方法以後,s0 和 s1 中各有 5 個元素。而後能夠對分割開的元素進行處理:

s0.forEachRemaining(i -> System.out.println(i));
複製代碼

SubList

ArrayList 中還有一個內部類 SubList。SubList 用於返回 ArrayList 的一部分元素,內部的操做方法與 ArrayList 基本一致,可是須要注意的是,對 SubList 的操做會直接影響到原 ArrarList。

fail-fast 機制

在 ArrayList 中,checkForComodification()ConcurrentModificationException() 使用的頻率很高。這個和 fail-fast 機制有關。

ArrayList 不是線程安全的,因此在對容器操做的過程當中,容器的元素倍其餘的操做或者線程修改以後,就會出現 ConcurrentModificationException 異常。checkForComodification() 方法就是用來檢查元素是否被修改。這個機制就稱之爲 fail-fast

後續會有其餘的文章來介紹 fail-fast

(完)

原文

相關文章

關注微信公衆號,聊點其餘的

相關文章
相關標籤/搜索