Java集合之ArrayList

會飛的小熊發表於2018-08-19

ArrayList類,是基於陣列實現的List,本質上對其的操作和對陣列的操作相同。本文旨在簡單介紹Java8中的部分實現、一些使用中應當注意的細節、以及JDK8中新添的“有趣”的特性。

下圖為ArrayList類的繼承關係:

ArrayList繼承圖

官方介紹

ArrayList 是 List 介面的大小可變陣列的實現。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 介面外,此類還提供一些方法來操作內部用來儲存列表的陣列的大小。(此類大致上等同於 Vector 類,除了此類是不同步的。)

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定時間執行。add 操作以分攤的固定時間執行,也就是說,新增 n 個元素需要 O(n) 時間。其他所有操作都以線性時間執行(大體上講)。與用於 LinkedList 實現的常數因子相比,此實現的常數因子較低。

每個 ArrayList 例項都有一個容量。該容量是指用來儲存列表元素的陣列的大小。它總是至少等於列表的大小。隨著向 ArrayList 中不斷新增元素,其容量也自動增長。並未指定增長策略的細節,因為這不只是新增元素會帶來分攤固定時間開銷那樣簡單。

在新增大量元素前,應用程式可以使用 ensureCapacity 操作來增加 ArrayList 例項的容量。這可以減少遞增式再分配的數量。

注意,此實現不是同步的。如果多個執行緒同時訪問一個 ArrayList 例項,而其中至少一個執行緒從結構上修改了列表,那麼它必須保持外部同步。(結構上的修改是指任何新增或刪除一個或多個元素的操作,或者顯式調整底層陣列的大小;僅僅設定元素的值不是結構上的修改。)這一般通過對自然封裝該列表的物件進行同步操作來完成。如果不存在這樣的物件,則應該使用 Collections.synchronizedList 方法將該列表“包裝”起來。這最好在建立時完成,以防止意外對列表進行不同步的訪問:
List list = Collections.synchronizedList(new ArrayList(...));
此類的 iterator 和 listIterator 方法返回的迭代器是Fail-fast的:在建立迭代器之後,除非通過迭代器自身的 remove 或 add 方法從結構上對列表進行修改,否則在任何時間以任何方式對列表進行修改,迭代器都會丟擲ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發生任意不確定行為的風險。

注意,迭代器的Fail-fast行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。Fail-fast迭代器會盡最大努力丟擲 ConcurrentModificationException。因此,為提高這類迭代器的正確性而編寫一個依賴於此異常的程式是錯誤的做法:迭代器的快速失敗行為應該僅用於檢測 bug。

靜態變數和例項變數

    // 預設的容量為10
    private static final int DEFAULT_CAPACITY = 10;

    // 預設容量空例項共用該空陣列例項
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 預設容量空例項共用該空陣列例項。使用DEFAULTCAPACITY_EMPTY_ELEMENTDA他的原因見下一個欄位
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 容量的最大值,一些虛擬機器在陣列中保留一些header words,嘗試分配更大的陣列時可能導致OutOfMemoryError
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 儲存ArrayList元素的陣列。陣列的長度即為ArrayList容量。
    // 在elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 且新增第一個元素時,
    // elementData 將被擴充到DEFAULT_CAPACITY 大小
    transient Object[] elementData; 

    // ArrayList的size
    private int size;
複製程式碼

建構函式

    // 帶有初始容量,大於0時直接建立對應大小的陣列
    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);
        }
    }

   // 未指定初始容量,使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA代表空陣列
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   // 使用容器初始化,儲存順序為該容器迭代器返回順序
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製程式碼

新增元素

    // 新增元素函式
    public boolean add(E e) {
        // 保證容量最小為 size + 1
        ensureCapacityInternal(size + 1);
        //元素新增到陣列末尾
        elementData[size++] = e;
        return true;
    }
    
    
    // 私有函式,保證儲存陣列elementData容量最小為minCapacity
    private void ensureCapacityInternal(int minCapacity) {
        // 如果未指定初始容量,則第一次擴容為預設的10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    
    
    // 私有函式,提供明確的最小容量
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 最小容量大於當前容量,擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    // 擴容函式
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 容量增加0.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 容量增加0.5倍後還是小於要求的容量,則使用要求的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 擴容後的容量大於最大容量要求,需要校驗,此處有兩種可能:
        // 一種是擴容的容量確實大於最大容量
        // 另一種可能就是容量增加0.5倍導致newCapacity溢位,即超出int的最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    // 校驗容量是否合法
    private static int hugeCapacity(int minCapacity) {
        // 容量超出int的最大值
        if (minCapacity < 0)
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
複製程式碼

1.8新特性

在Java8版本中,增強了對函數語言程式設計的支援,在ArrayList中當然也必不可少。下面介紹兩個方便操作的方法,體會下函數語言程式設計的魅力(此時推薦下《Java8簡明教程》)。

forEach

Java中的foreach和此處的forEach雖然在執行的結果上來看區別不大,但是執行的機制可大不相同,具體程式碼如下:

public class ConsumerTest {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println("forEach:");
        list.forEach((a) -> System.out.printf(a + " "));

        System.out.println();
        System.out.println("foreach:");
        for (Integer a : list)
            System.out.printf(a + " ");
    }

}

複製程式碼

執行結果:

forEach:
1 2 3 4 5 
foreach:
1 2 3 4 5 
複製程式碼

removeIf

在Java8之前,如果你想刪除ArrayList中符合特定條件的資料,會怎麼做?
迴圈遍歷刪除的話,ArrayList在刪除時會改變索引。使用foreach刪除?那你的程式執行的好嗎(手動笑)。
使用removeIf方法,讓我們來看看什麼叫爽:

public class ConsumerTest {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println("removeIf 前:");
        list.forEach((a) -> System.out.printf(a + " "));

        list.removeIf((a) -> a % 2 == 0);

        System.out.println("");
        System.out.println("removeIf 後:");
        list.forEach((a) -> System.out.printf(a + " "));
    }

}
複製程式碼

執行結果:

removeIf 前:
1 2 3 4 5 
removeIf 後:
1 3 5 
複製程式碼

上述程式碼刪除ArrayList中可以被2整除的整數,只需要一行程式碼搞定。

相關文章