Java集合(1):ArrayList深度解析

一、ArrayList 的概述與特點

ArrayList就是動態數組,是數組Array的複雜版本,它具有以下特點:
(1)是一個動態數組,支持動態擴容
(2)有序存儲,存儲的元素可重複,並支持null元素的存儲
(3)底層爲數組,查找快,增刪慢
(4)不支持同步,線程不安全



二、ArrayList 的繼承體系

查看源碼,發現ArrayList繼承AbstractList抽象父類,實現了List、RandomAccess、Cloneable、Serializable接口。
其中:
(1)List接口:定義了List的一些操作規範。至於爲什麼繼承了AbstractList還要再實現List接口的問題,網上有兩種說法:一種是代碼規範,方便直接看出是個List集合;二是爲了是實現代理時,獲得的接口中有List接口。
(2)RandomAccess接口:是一個空接口,代表可隨機訪問。實現了此接口的集合使用for循環遍歷速度更快,而沒有實現此接口的集合使用iterator迭代器遍歷速度更快。
(3)Cloneable接口:空接口,實現此接口是爲了可以調用clone()方法,ArrayList的clone()方法屬於淺克隆。
(4)Serializable接口:空接口,代表可序列化
ArrayList繼承體系





三、重要屬性

(1)transient Object[] elementData;這是ArrayList的底層,是一個object數組。由transient修飾,代表此數組不參與序列化,而是使用另外的序列化方式(使用writeObject方法進行序列化,只序列化有值的位置,其他未賦值的位置不進行序列化,節省空間)。
(2)private int size;指集合包含的元素數量,注意與elementData.length(集合容量)的區別。比如當前集合容量爲10,只存儲了6個元素,則size爲6,elementData.length爲10。

四、構造方法

查看ArrayList源碼所知,ArrayList有三個構造方法,一種爲創建指定容量的集合,一種爲無參的構造方法,最後一種爲構造一個包含指定集合的構造方法,代碼都比較簡單,重點看一下無參的構造方法:

	/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
   
   
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空的集合。注意,雖然在JDK1.8的註釋中所說無參構造器創建的是一個初始容量爲10的空集合。但是查看代碼可知,剛開始創建的時候只是一個空集合,而容量變爲10體現在後續的操作中(下文有介紹)。

五、增刪改查

(1)add 方法

add方法有兩個,分別public boolean add(E e)public void add(int index, E element),第一個是在集合元素的最後添加一個元素,第二個方法爲在指定位置添加元素。代碼如下:

public boolean add(E e) {
   
   
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
public void add(int index, E element) {
   
   
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

由以上代碼,我們可以發現兩種add方法都調用了ensureCapacityInternal(size + 1)方法,此方法目的是爲了確定是否擴容,第一個add方法比較簡單,先判讀是否擴容,然後再讓最後一個元素的下一個等於需要添加的元素,重點看一下第二個add方法:

a.首先檢查索引是否越界,如果越界則拋異常。調用的是rangeCheckForAdd(index);

    private void rangeCheckForAdd(int index) {
   
   
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

b.然後檢測是否需要擴容,注意,擴容原理是比較重要的,調用的是ensureCapacityInternal(size + 1);注意,這裏傳入的參數minCapacity爲數組存儲的元素+1。

    private void ensureCapacityInternal(int minCapacity) {
   
   
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

這裏首先調用的是calculateCapacity(elementData, minCapacity),用來計算容量:

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

代碼的流程是如果現在的集合是空的,那麼讓它的容量等於DEFAULT_CAPACITY(值爲10),否則返回minCapacity(值爲size+1)。

然後調用的ensureExplicitCapacity(int minCapacity)方法,代碼如下:

    private void ensureExplicitCapacity(int minCapacity) {
   
   
        modCount++;

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

其中modCount代表修改次數,是用在迭代器裏保證遍歷過程中集合不被其他線程修改。後面的代碼很好理解,如果size+1(已經存儲的元素+要存儲的元素)大於集合容量,則調用擴容方法grow(minCapacity)

擴容方法grow(minCapacity)代碼:

    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);
    }

舊的容量等於elementData.length,新的容量等於oldCapacity + (oldCapacity >> 1),這個地方新的容量等於舊的容量的1.5倍。從這裏可以看出,動態擴容每次擴容1.5倍。

如果新容量小於size+1,返回size+1。這裏適用的情況是第一次添加時elementData.length=0,那麼舊容量和計算出的新容量都爲0,而minCapacity 爲之前得出的10,所以把10賦值給新容量。

然後則是對新容量如果超過數組最大長度的處理,最後調用了Arrays.copyOf(elementData, newCapacity)進行數組拷貝,跟蹤代碼,發現數組拷貝最終調用的是System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法,是一個native方法。

講完了動態擴容的ensureCapacityInternal(size + 1)代碼,後面的比較簡單了,就是數組的複製,要添加元素的賦值,還有讓size加一。

(2)remove 方法
由於刪除不需要涉及擴容,所以代碼比較簡單,自行閱讀即可。ArrayList提供了三種刪除元素的方法,分別爲remove(int index)remove(Object o)fastRemove(int index),即刪除指定位置的元素、刪除第一次出現的指定元素、跳過邊界檢查並且不返回刪除的值的快速刪除方法。
(2)setget方法
改查方法只需要檢查是否索引越界,然後進行改查操作即可,代碼也非常簡單,不再贅述。


六、其他重要方法

(1)ensureCapacity(int minCapacity)
看這個方法的名字就是一個擴容的方法。我們從上文可知,每次進行擴容時都要進行一次數組複製,所以頻繁的擴容會導致性能的浪費,所以最好的辦法是在創建數組後就用此方法設置一個容量以減少擴容的次數。

public void ensureCapacity(int minCapacity) {
   
   
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
   
   
            ensureExplicitCapacity(minCapacity);
        }
    }

(2)trimToSize()
數組在複製完成後,它的size極有可能是小於它的容量的,爲了減少空間的浪費,可以調用此方法來清空後面沒有賦值的容量。

public void trimToSize() {
   
   
        modCount++;
        if (size < elementData.length) {
   
   
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

(3)clear()
如果一個ArrayList不再使用,可以調用clear()方法,讓每個元素都爲null,方便JVM回收。

public void clear() {
   
   
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

(4)線程安全問題
由於ArrayList在多線程環境下非安全的,多個線程同時操作時會產生併發問題,解決辦法:
a.使用Collections工具類的方法:

 List list = Collections.synchronizedList(new ArrayList(...)); 

b.使用線程安全類Vector

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