Java集合源碼分析之List(二):ArrayList_一點課堂(多岸學院)

作了這麼多準備,終於到了ArrayList了,ArrayList是咱們使用最爲頻繁的集合類了,咱們先看看文檔是如何介紹它的:java

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)面試

可見,ArrayListVector的翻版,只是去除了線程安全。Vector由於種種緣由不推薦使用了,這裏咱們就不對其進行分析了。ArrayList是一個能夠動態調整大小的List實現,其數據的順序與插入順序始終一致,其他特性與List中定義的一致。數組

ArrayList繼承結構

file

能夠看到,ArrayListAbstractList的子類,同時實現了List接口。除此以外,它還實現了三個標識型接口,這幾個接口都沒有任何方法,僅做爲標識表示實現類具有某項功能。RandomAccess表示實現類支持快速隨機訪問,Cloneable表示實現類支持克隆,具體表現爲重寫了clone方法,java.io.Serializable則表示支持序列化,若是須要對此過程自定義,能夠重寫writeObjectreadObject方法。安全

通常面試問到與ArrayList相關的問題時,可能會問ArrayList的初始大小是多少?不少人在初始化ArrayList時,可能都是直接調用無參構造函數,從未關注過此問題。例如,這樣獲取一個對象:dom

ArrayList<String> strings = new ArrayList<>();

咱們都知道,ArrayList是基於數組的,而數組是定長的。那ArrayList爲什麼不須要指定長度,就能使咱們既能夠插入一條數據,也能夠插入一萬條數據?回想剛剛文檔的第一句話:ide

Resizable-array implementation of the List interface.函數

ArrayList能夠動態調整大小,因此咱們才能夠無感知的插入多條數據,這也說明其必然有一個默認的大小。而要想擴充數組的大小,只能經過複製。這樣一來,默認大小以及如何動態調整大小會對使用性能產生很是大的影響。咱們舉個例子來講明此情形:性能

好比默認大小爲5,咱們向ArrayList中插入5條數據,並不會涉及到擴容。若是想插入100條數據,就須要將ArrayList大小調整到100再進行插入,這就涉及一次數組的複製。若是此時,還想再插入50條數據呢?那就得把大小再調整到150,把原有的100條數據複製過來,再插入新的50條數據。自此以後,咱們每向其中插入一條數據,都要涉及一次數據拷貝,且數據量越大,須要拷貝的數據越多,性能也會迅速降低。學習

其實,ArrayList僅僅是對數組操做的封裝,裏面採起了必定的措施來避免以上的問題,若是咱們不利用這些措施,就和直接使用數組沒有太大的區別。那咱們就看看ArrayList用了哪些措施,而且如何使用它們吧。咱們先從初始化提及。優化

構造方法與初始化

ArrayList一共有三個構造方法,用到了兩個成員變量。

//這是一個用來標記存儲容量的數組,也是存放實際數據的數組。
//當ArrayList擴容時,其capacity就是這個數組應有的長度。
//默認時爲空,添加進第一個元素後,就會直接擴展到DEFAULT_CAPACITY,也就是10
//這裏和size區別在於,ArrayList擴容並非須要多少就擴展多少的
transient Object[] elementData;

//這裏就是實際存儲的數據個數了
private int size;

除了以上兩個成員變量,咱們還須要掌握一個變量,它是

protected transient int modCount = 0;

這個變量主要做用是防止在進行一些操做時,改變了ArrayList的大小,那將使得結果不可預測。

下面咱們看看構造函數:

//默認構造方法。文檔說明其默認大小爲10,但正如elementData定義所言,
//只有插入一條數據後纔會擴展爲10,而實際上默認是空的
 public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//帶初始大小的構造方法,一旦指定了大小,elementData就再也不是原來的機制了。
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    }
}

//從一個其餘的Collection中構造一個具備初始化數據的ArrayList。
//這裏能夠看到size是表示存儲數據的數量
//這也展現了Collection這種抽象的魅力,能夠在不一樣的結構間轉換
public ArrayList(Collection<? extends E> c) {
    //轉換最主要的是toArray(),這在Collection中就定義了
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

重要方法

ArrayList已是一個具體的實現類了,因此在List接口中定義的全部方法在此都作了實現。其中有些在AbstractList中實現過的方法,在這裏再次被重寫,咱們稍後就能夠看到它們的區別。

先看一些簡單的方法:

//還記得在AbstractList中的實現嗎?那是基於Iterator完成的。
//在這裏徹底不必先轉成Iterator再進行操做
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

//和indexOf是相同的道理
 public int lastIndexOf(Object o) {
    //...
}

//同樣的道理,已經有了全部元素,不須要再利用Iterator來獲取元素了
//注意這裏返回時把elementData截斷爲size大小
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

//帶類型的轉換,看到這裏a[size] = null;這個用處真不大,除非你肯定全部元素都不爲空,
//才能夠經過null來判斷獲取了多少有用數據。
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 給定的數據長度不夠,複製出一個新的並返回
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

數據操做最重要的就是增刪改查,改查都不涉及長度的變化,而增刪就涉及到動態調整大小的問題,咱們先看看改和查是如何實現的:

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

//只要獲取的數據位置在0-size之間便可
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

//改變下對應位置的值
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

增和刪是ArrayList最重要的部分,這部分代碼須要咱們細細研究,咱們看看它是如何處理咱們例子中的問題的:

//在最後添加一個元素
public boolean add(E e) {
    //先確保elementData數組的長度足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index);

    //先確保elementData數組的長度足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //將數據向後移動一位,空出位置以後再插入
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    elementData[index] = element;
    size++;
}

以上兩種添加數據的方式都調用到了ensureCapacityInternal這個方法,咱們看看它是如何完成工做的:

//在定義elementData時就提過,插入第一個數據就直接將其擴充至10
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    
    //這裏把工做又交了出去
    ensureExplicitCapacity(minCapacity);
}

//若是elementData的長度不能知足需求,就須要擴充了
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

//擴充
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //能夠看到這裏是1.5倍擴充的
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    //擴充完以後,仍是沒知足,這時候就直接擴充到minCapacity
    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的擴容機制了。首先建立一個空數組elementData,第一次插入數據時直接擴充至10,而後若是elementData的長度不足,就擴充1.5倍,若是擴充完還不夠,就使用須要的長度做爲elementData的長度。

這樣的方式顯然比咱們例子中好一些,可是在遇到大量數據時仍是會頻繁的拷貝數據。那麼如何緩解這種問題呢,ArrayList爲咱們提供了兩種可行的方案:

  • 使用ArrayList(int initialCapacity)這個有參構造,在建立時就聲明一個較大的大小,這樣解決了頻繁拷貝問題,可是須要咱們提早預知數據的數量級,也會一直佔有較大的內存。
  • 除了添加數據時能夠自動擴容外,咱們還能夠在插入前先進行一次擴容。只要提早預知數據的數量級,就能夠在須要時直接一次擴充到位,與ArrayList(int initialCapacity)相比的好處在於沒必要一直佔有較大內存,同時數據拷貝的次數也大大減小了。這個方法就是ensureCapacity(int minCapacity),其內部就是調用了ensureCapacityInternal(int minCapacity)

其餘還有一些比較重要的函數,其實現的原理也大同小異,這裏咱們不一一分析了,但仍是把它們列舉出來,以便使用。

//將elementData的大小設置爲和size同樣大,釋放全部無用內存
public void trimToSize() {
    //...
}

//刪除指定位置的元素
public E remove(int index) {
    //...
}

//根據元素自己刪除
public boolean remove(Object o) {
    //...
}

//在末尾添加一些元素
public boolean addAll(Collection<? extends E> c) {
    //...
}

//從指定位置起,添加一些元素
public boolean addAll(int index, Collection<? extends E> c){
    //...
}

//刪除指定範圍內的元素
protected void removeRange(int fromIndex, int toIndex){
    //...
}

//刪除全部包含在c中的元素
public boolean removeAll(Collection<?> c) {
    //...
}

//僅保留全部包含在c中的元素
public boolean retainAll(Collection<?> c) {
    //...
}

ArrayList還對父級實現的ListIterator以及SubList進行了優化,主要是使用位置訪問元素,咱們就再也不研究了。

其餘實現方法

ArrayList不只實現了List中定義的全部功能,還實現了equalshashCodeclonewriteObjectreadObject等方法。這些方法都須要與存儲的數據配合,不然結果將是錯誤的或者克隆獲得的數據只是淺拷貝,或者數據自己不支持序列化等,這些咱們定義數據時注意到便可。咱們主要看下其在序列化時自定義了哪些東西。

//這裏就能解開咱們的迷惑了,elementData被transient修飾,也就是不會參與序列化
//這裏咱們看到數據是一個個寫入的,而且將size也寫入了進去
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

        // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    //modCount的做用在此體現,若是序列化時進行了修改操做,就會拋出異常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

readObject是一個相反的過程,就是把數據正確的恢復回來,並將elementData設置好便可,感興趣能夠自行閱讀源碼。

總結

整體而言,ArrayList仍是和數組同樣,更適合於數據隨機訪問,而不太適合於大量的插入與刪除,若是必定要進行插入操做,要使用如下三種方式:

  • 使用ArrayList(int initialCapacity)這個有參構造,在建立時就聲明一個較大的大小。
  • 使用ensureCapacity(int minCapacity),在插入前先擴容。
  • 使用LinkedList,這個無可厚非哈,咱們很快就會介紹這個適合於增刪的集合類。

【感謝您能看完,若是可以幫到您,麻煩點個贊~】

更多經驗技術歡迎前來共同窗習交流: 一點課堂-爲夢想而奮鬥的在線學習平臺 http://www.yidiankt.com/

![關注公衆號,回覆「1」免費領取-【java核心知識點】] file

QQ討論羣:616683098

QQ:3184402434

想要深刻學習的同窗們能夠加我QQ一塊兒學習討論~還有全套資源分享,經驗探討,等你哦! 在這裏插入圖片描述

相關文章
相關標籤/搜索