ArrayList詳解(基於JDK8)

ArrayList

要點

  • ArrayList 內部使用動態數組實現元素存儲。並允許所有元素,包括 null,元素可重複。

  • 實現了 RandomAccess 接口,可實現快速隨機訪問。

  • 每次添加元素時都會檢查動態元素數組容量是否足夠,如果不夠則會動態擴容,擴容是擴容 1.5 倍,擴容後會複製原來的數組,這個開銷很大,所以一定要根據業務場景指定初始化容量。添加元素效率視情況而定,可能會面臨數組擴容與內容複製等開銷問題。指定位置的插入與刪除元素效率低,因爲需要移動其它元素。

  • 動態數組是通過 transient 修飾的,默認不被序列化(因爲動態數組可能沒有存滿),ArrayList 自定義了序列化與反序列化的方法保證只對數組中的有效元素進行序列化。

  • ArrayList 的迭代器會返回一個 內部類 Itr 對象迭代時刪除元素應該使用迭代器的 remove 方法而非 ArrayList 本身的 remove 方法,否則會產生 Fail Fast 異常。當然普通使用 ArrayList 的 remove 方法是沒問題的。

  • modCount 屬性是繼承自 AbstractList 的,用來記錄 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的所有操作,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。如果迭代或者序列化會檢查 modCount 版本,如果不一致則會產生 Fail Fast 異常

  • ArrayList 是線程不安全的,建議在單線程中才使用 ArrayList,多線程可以使用 Vector 類、Collections.synchronizedList、JUC 的 CopyOnWriteArrayList 類等方法解決併發安全問題。

  • 支持隨機訪問,按照索引進行訪問的效率很高,效率爲 O(1)。

  • 按照內容查找元素效率較低,效率爲 O(N)。

ArrayList類API

ArrayList 對象不能存儲基本類型,只能存儲引用類型的數據。類似 <int> 不能寫,想要存儲基本類型數據,<> 中的數據類型,需要使用基本類型包裝類,如<Integer> 也可能用到基本數據類型,JVM會根據場景進行自動拆箱、自動裝箱。

public boolean add(E obj); 				// 將指定元素添加到此集合的尾部
public boolean add(int index, E obj); 	// 在指定位置插入元素,後面的元素往後移動
public E remove(int index); 			// 刪除指定位置上的元素,返回被刪除的元素
public E get(int index);    			// 返回指定位置上的元素
public int size();  					// 返回此集合中的元素數目
public void trimToSize();  	// 將數組列表的存儲容量削減到當前尺寸,確保數組不會有新元素添加的時候調用
public void set(int index, E obj);		// 設置數組列表指定位置的值,覆蓋原有內容。
ArrayList<Employee> staff = new ArrayList<Employee>();
// 右邊的類型參數可省並指定初始大小 一定要寫,避免多次自動擴容影響性能
ArrayList<Employee> staff = new ArrayList<>(100);  
// 遍歷方法
for(Employee e : staff){
    e.raiseMoney(300);
}

for(int i = 0; i < staff.size(); i++){
    staff.get(i).raiseMoney(300);
}

源碼分析

下面的分析基於 JDK8。最新的 ArrayList 源碼是有改動的。

因爲 ArrayList 是基於數組實現的,所以支持快速隨機訪問。RandomAccess 接口標識着該類支持快速隨機訪問,RandomAccess 是一個標記接口,沒有任何方法。

1. 基本屬性
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 版本號
    private static final long serialVersionUID = 8683452581122892189L;
    // 默認容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空對象數組
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 缺省空對象數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 存放元素數組
    transient Object[] elementData;
    // 實際元素大小,默認爲0
    private int size;
    // 最大數組容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

實現了 RandomAccess 接口,代表該類支持快速隨機訪問,因爲 ArrayList 是基於數組實現的。

存放元素數組爲 elementData,其默認大小爲 DEFAULT_CAPACITY = 10。如果初始化時沒有指定數組大小,那麼第一次添加元素時會擴容,這時候就會擴容到默認的 10,如果指定了容量,那就按容量 1.5 擴容了。

在這裏插入圖片描述

2. 初始化

主要是初始化一個指定大小的數組

/**
 * 不帶參數的構造方法
 */
public ArrayList() {
    // 直接將空數組賦給elementData
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 帶有容量initialCapacity的構造方法
 *
 * @param 初始容量列表的初始容量
 * @throws IllegalArgumentException 如果指定容量爲負
 */
public ArrayList(int initialCapacity) {
    // 如果初始化時ArrayList大小大於0
    if (initialCapacity > 0) {
        // new一個該大小的object數組賦給elementData
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { // 如果大小爲0
        // 將空數組賦給elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else { // 小於0
        // 則拋出IllegalArgumentException異常
        throw new IllegalArgumentException("Illegal Capacity: " +
                                           initialCapacity);
    }
}
3. 添加元素

添加元素的 add 方法如下。默認是添加到數組最後的位置的。這樣添加還是很快的。

/**
 * 添加一個值,首先會確保容量
 *
 * @param e 要添加到此列表中的元素
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    // 擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 將e賦值給elementData的size+1的位置
    elementData[size++] = e;
    return true;
}

加元素時使用 ensureCapacityInternal() 方法來保證容量足夠

/**
 * 得到最小擴容量
 *
 * @param minCapacity
 */
private void ensureCapacityInternal(int minCapacity) {
    // 調用另一個方法
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 首先計算capacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果之前爲空則擴容到DEFAULT_CAPACITY爲10
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * 判斷是否需要擴容
 *
 * @param minCapacity
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 增加結構修改計數器
    modCount++;
    // 如果最小需要空間比elementData的內存空間要大,則需要擴容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

如果不夠時,需要使用 grow() 方法進行擴容,新容量的大小爲 oldCapacity + (oldCapacity >> 1),也就是舊容量的 1.5 倍。elementData 數組會隨着實際元素個數的增多而重新分配。

/**
 * 擴容,以確保它可以至少持有由參數指定的元素的數目
 *
 * @param minCapacity 當前所需的最小容量
 */
private void grow(int minCapacity) {
    // 原始數據數組長度
    int oldCapacity = elementData.length;
    // 擴容至原來的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 再判斷一下新數組的容量夠不夠,夠了就直接使用這個長度創建新數組,
    // 不夠就將數組長度設置爲需要的長度
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 若預設值大於默認的最大值檢查是否溢出
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 調用Arrays.copyOf方法將elementData數組指向新的內存空間時newCapacity的連續空間
    // 並將elementData的數據複製到新的內存空間
    elementData = Arrays.copyOf(elementData, newCapacity);
}

擴容操作需要調用 Arrays.copyOf() 把原數組整個複製到新數組中,這個操作代價很高,因此最好在創建 ArrayList 對象時就指定大概的容量大小,減少擴容操作的次數

也可以在指定索引處添加元素。這裏需要的操作是將 index 位置及以後的元素搬運到 index + 1之後,這個操作開銷挺大

/**
 * 在ArrayList的index位置,添加元素element,會檢查添加的位置和容量
 *
 * @param index   指定元素將被插入的索引
 * @param element 要插入的元素
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    // 判斷index是否越界
    rangeCheckForAdd(index);
    // 擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    // src:源數組; srcPos:源數組要複製的起始位置; dest:目的數組; destPos:目的數組放置的起始位置; length:複製的長度
    // 將elementData從index位置開始,複製到elementData的index+1開始的連續空間
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 在elementData的index位置賦值element
    elementData[index] = element;
    // ArrayList的大小加一
    size++;
}
4. 獲取元素

獲取元素用 get 方法。即直接通過索引獲取對應位置的元素,速度極快。

public E get(int index) {
    // 檢查輸入的索引有沒有越界
    rangeCheck(index);
	// 返回數據數組指定索引的元素
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

indexOf 用於返回一個值在數組首次出現的位置,會根據是否爲 null 使用不同方式判斷。不存在就返回 -1。時間複雜度爲O(N)。

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

lastIndexOf 方法效果類似,只是是反過來從後面找索引位置。

5. 更新元素

更新元素依然是通過索引來的。更新之後會返回原始索引處的舊元素。

public E set(int index, E element) {
    rangeCheck(index);	// 索引有效性檢查
	// 獲取舊元素
    E oldValue = elementData(index);
    // 設置新元素
    elementData[index] = element;
    // 返回舊元素
    return oldValue;
}
6. 基本方法

下面是一些很基礎的方法。

public int size() {
    return size;
}
public boolean isEmpty() {
    return size == 0;
}
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
7. 序列化

ArrayList 基於數組實現,並且具有動態擴容特性,因此保存元素的數組不一定都會被使用,那麼就沒必要全部進行序列化。

保存元素的數組 elementData 使用 transient 修飾,該關鍵字聲明數組默認不會被序列化

transient Object[] elementData; // non-private to simplify nested class access

ArrayList 實現了 writeObject() 和 readObject() 來控制只序列化數組中有元素填充那部分內容。數組沒有存元素的部分不序列化。當寫入到輸出流時,先寫入**“容量”**,再依次寫入“每一個元素”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。

注意:序列化時也會檢查 modCount,如果序列化時併發修改列表,可能造成 fail fast 而拋異常。

/**
 * 保存數組實例的狀態到一個流(即序列化)。寫入過程數組被更改會拋出異常
 *
 * @serialData The length of the array backing the <tt>ArrayList</tt>
 * instance is emitted (int), followed by all of its elements
 * (each an <tt>Object</tt>) in the proper order.
 */
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    // 執行默認的反序列化/序列化過程。將當前類的非靜態和非瞬態字段寫入此流
    s.defaultWriteObject();

    // 寫入大小
    s.writeInt(size);

    // 按順序寫入所有元素
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
/**
 * 從流中重構ArrayList實例(即反序列化)。
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // 執行默認的序列化/反序列化過程
    s.defaultReadObject();

    // 讀入數組長度
    s.readInt(); // ignored

    if (size > 0) {
        // 像clone()方法 ,但根據大小而不是容量分配數組
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        //讀入所有元素
        for (int i = 0; i < size; i++) {
            a[i] = s.readObject();
        }
    }
}

序列化時需要使用 ObjectOutputStream 的 writeObject() 將對象轉換爲字節流並輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實現序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理類似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
8. 刪除元素

需要調用 System.arraycopy() 將 index + 1 後面的元素都複製到 index 位置上,該操作的時間複雜度爲 O(N),可以看出 ArrayList 刪除元素的代價是非常高的。

注意:remove 操作會修改 modCount 值。

public E remove(int index) {
    // 判斷是否越界
    rangeCheck(index);
    
    // remove操作會修改modCount值
    modCount++;
    // 讀取舊值
    E oldValue = elementData(index);
    // 獲取index位置開始到最後一個位置的個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 將elementData數組index+1位置開始拷貝到elementData從index開始的空間
        System.arraycopy(elementData, index + 1, elementData, index,
                         numMoved);
    // 使size-1 ,設置elementData的size位置爲空,讓GC來清理內存空間
    elementData[--size] = null; // 便於垃圾回收器回收

    return oldValue;
}
9. 迭代與刪除

迭代器的常見誤用就是在迭代的中間調用容器的刪除方法。

List<String> list = new ArrayList<>();
list.add("str1");
list.add("str2");
list.add("str3");
for (String s : list) {
    if ("str1".equals(s)) {
        // 這裏使用了List接口提供的remove方法
        list.remove(s);
    }
}

這段代碼看起來好像沒有什麼問題,但是如果我們運行,就會拋出 ConcurrentModificationException 異常。

其實這不是特例,每當我們使用迭代器遍歷元素時,如果修改了元素內容(添加、刪除元素),就會拋出異常,由於 foreach 同樣使用的是迭代器,所以也有同樣的情況。

返回迭代器源碼。

public Iterator<E> iterator() {
    return new Itr();
}

返回的是一個內部類 Itr 的對象。

這個內部類如下:

/**
 * 通用的迭代器實現
 */
private class Itr implements Iterator<E> {
    int cursor;       // 遊標,下一個元素的索引,默認初始化爲0
    int lastRet = -1; // 上次訪問的元素的位置
    
    // 記錄獲取迭代器時的modCount,迭代過程不運行修改數組,否則就拋出異常
    int expectedModCount = modCount;

    // 是否還有下一個
    public boolean hasNext() {
        return cursor != size;
    }

    // 下一個元素
    @SuppressWarnings("unchecked")
    public E next() {
        // 檢查數組是否被修改
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;// 向後移動遊標
        return (E) elementData[lastRet = i];// 設置訪問的位置並返回這個值
    }

    // 刪除元素
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification(); // 檢查數組是否被修改
		// 用迭代器的刪除方法會自己更新modCount值
        try {
            // 這裏調用ArrayList自身的remove方法,這個方法會修改modCount值
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            // 修改迭代器內部的expectedModCount爲刪除後當前最新的modCount值,這樣就不會拋異常
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        // 忽略....
    }

    // 檢查數組是否被修改:就是判斷當前列表的modCount與生成迭代器時的modCount是否一致
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

可以明顯的看到共有兩個remove()方法,一個屬於 ArrayList 本身,還有一個屬於其內部類 Itr

ArrayList 類中有一個 modCount 屬性,這個屬性是繼承自 AbstractList,其保存了我們對 ArrayList 進行的的操作次數,當我們添加或者刪除元素時,modeCount 都會進行對應次數的增加。相當於記錄了結構性變化,即添加、插入、刪除元素,只是修改元素的內容不算結構性變化。

在我們使用 ArrayList 的 iterator() 方法獲取到迭代器進行遍歷時,會把 ArrayList 當前狀態下的 modCount 賦值給 Itr 類的 expectedModCount 屬性,相當於創建迭代器時候對 modCount 的一個版本快照。如果我們在迭代過程中,使用了 ArrayList 的 remove()add()方法,這時 modCount 就會加 1 ,但是迭代器中的 expectedModeCount 並沒有變化,當我們再使用迭代器的next()方法時,它會調用checkForComodification()方法,通過對比發現現在的 modCount 已經與 expectedModCount 不一致了,則會拋出ConcurrentModificationException異常。

但是如果使用內部類 Itr 迭代器提供的remove()方法,它會調用 ArrayList 提供的 remove方法,同時還有一個操作:expectedModCount = modCount;,這會修改當前迭代器內部記錄的 expectedModCount 的值,所以就不會存在版本不一致問題。

綜上:在單線程的遍歷過程中,如果要進行 remove 操作,應該調用迭代器的 remove 方法而不是集合類的 remove 方法。

PS:這裏討論的是迭代刪除時使用 ArrayList 的 remove 方法,普通使用 ArrayList 的 remove 方法是沒問題的。

Fail-Fast 機制

1. 概述

通過上面的例子可以引出 Fail-fast 機制,即快速失敗機制,是 Java 集合(Collection)中的一種錯誤檢測機制。當在序列化或者迭代集合的過程中該集合在結構上發生改變的時候,就有可能會發生 fail-fast,即拋出 ConcurrentModificationException 異常。

Fail-fast 機制並不保證在不同步的修改下一定會拋出異常,它只是盡最大努力去拋出,所以這種機制一般僅用於檢測 bug。

在我們常見的 Java 集合中就可能出現 fail-fast 機制,比如 ArrayList,HashMap。

多線程和單線程環境下都有可能出現 Fail-fast。

modCount 屬性是繼承自 AbstractList 的,用來記錄 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的所有操作,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。可以用於檢查併發修改的情況。

protected transient int modCount = 0;

modCount 此字段由 iteratorlistiterator 方法返回的迭代器和列表迭代器實現使用。子類是否使用此字段是可選的。

如果子類希望提供快速失敗迭代器(和列表迭代器),則它只需在其 add(E e) 和 remove(int) 方法(以及它所重寫的、導致列表結構上修改的任何其他方法)中增加此字段。

2. 避免fail-fast

方法1

單線程的遍歷過程中,如果要進行 remove 操作,應該調用迭代器的 remove 方法而不是集合類的 remove 方法。

方法2

使用併發包 (java.util.concurrent) 中的類來代替 ArrayList 和 HashMap。如 CopyOnWriterArrayList 代替 ArrayList。使用 ConcurrentHashMap 替代 HashMap。

Arrays.asList()

Arrays 類的靜態方法 asList() 將數組轉爲集合

String[] str = new String[]{"1","2","3"};
List aslist = Arrays.asList(str);
aslsit.add("4");
// Exception in thread "main" java.lang.UnsupportedOperationException
//    at java.util.AbstractList.add(Unknown Source)
//    at java.util.AbstractList.add(Unknown Source)
//    at test.LinkedListTest.main(LinkedListTest.java:13)

其實 asList() 返回的是 java.util.Arrays.ArrayList 對象,不是上述的 ArrayList 類!!!!

看 Arrays 類的部分源碼

public class Arrays {
    
    // 省略其他方法
 
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
        
    // 就是這個傢伙             👇
    private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable{
    
        private final E[] a;
    
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
    
        @Override
        public int size() {
            return a.length;
        }
        //省略其他方法
    }
}

Arrays.ArrayList 是工具類 Arrays 的一個內部靜態類,它沒有完全實現 List 的方法,而 ArrayList 直接實現了List 接口,實現了 List 所有方法。Arrays.ArrayList 是一個定長集合,因爲它沒有重寫 add, remove 方法,所以一旦初始化元素後,集合的 size 就是不可變的。所以使用這種方法會拋 UnsupportedOperationException 異常。

正確的使用方式:

String[] str = new String[]{"1","2","3"};
ArrayList al = new ArrayList(Arrays.asList(str)); // 將數組元素添加到集合的一種快捷方式

線程安全

ArrayList 底層是以數組方式實現的,實現了可變大小的數組,它允許所有元素,包括 null。如果開啓多個線程同時操作 List 集合,向 ArrayList 中增加元素,同時去除元素可能會出現一些問題,如數組下標越界異常。

在多線程情況下操作ArrayList 並不是線性安全的。

如何解決?

① 使用 Vertor 集合。

② 使用 Collections.synchronizedList。它會自動將我們的 list 方法進行改變,最後返回給我們一個加鎖了的 List。

protected static List<Object> arrayListSafe2 = Collections.synchronizedList(new ArrayList<Object>());  

③ 使用 JUC 中的 CopyOnWriteArrayList 類進行替換。

參考資料

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