1. 程式人生 > >ArrayList原始碼分析-jdk11 (18.9)

ArrayList原始碼分析-jdk11 (18.9)

[TOC](ArrayList 原始碼分析-jdk11 (18.9)) ## 1.概述 `ArrayList` 是一種變長的集合類,基於定長陣列實現。ArrayList 允許空值和重複元素,當往 ArrayList 中新增的元素數量大於其底層陣列容量時,其會通過擴容機制重新生成一個更大的陣列。另外,由於 ArrayList 底層基於陣列實現,所以其可以保證在 `O(1)` 複雜度下完成隨機查詢操作。其他方面,ArrayList 是非執行緒安全類,併發環境下,多個執行緒同時操作 ArrayList,會引發不可預知的錯誤。 ArrayList 是大家最為常用的集合類,作為一個變長集合類,其核心是擴容機制。所以只要知道它是怎麼擴容的,以及基本的操作是怎樣實現就夠了。本文後續內容將圍繞`jdk11 (18.9)`中ArrayList的原始碼展開敘述。 ## 2.原始碼分析 ### 2.1引數 1、ArrayList預設容量為10`DEFAULT_CAPACITY註釋` 2、ArrayList並不是在初始化的時候就建立了 `DEFAULT_CAPACITY=10` 的陣列。而是在往裡邊 `add` 第一個資料的時候會擴容到 10 `elementData註釋` ```java private static final long serialVersionUID = 8683452581122892189L; /** * 預設初始化容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 用於空例項的共享空陣列例項。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. * 用於預設大小的空例項的共享空陣列例項。我們將其與EMPTY_ELEMENTDATA分開來,以瞭解新增第一個元素時要膨脹多少。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. 儲存ArrayList元素的陣列緩衝區,ArrayList的容量是這個陣列緩衝區的長度。當第一個元素被新增的時候,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 將被擴充套件成 DEFAULT_CAPACITY */ transient Object[] elementData; // non-private to simplify nested class access /** * The size of the ArrayList (the number of elements it contains). * 陣列大小 * @serial */ private int size; ``` ### 2.2 構造方法 ArrayList 有三個構造方法,`無參構造方法`、`構造空的具有特定初始容量值方法`、`構造一個包含指定集合元素的列表,按照集合的迭代器返回它們的順序`。 #### 2.2.1 無參構造方法 注意下圖中的註釋`Constructs an empty list with an initial capacity of ten` 呼叫無參構造方法,預設構造一個容量為10的空list. ```java /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } ``` #### 2.2.2 構造空的具有特定初始容量值方法 1、在知道將會向 ArrayList 插入多少元素的情況下 2、在有大量資料寫入 時;**一定要初始化指定長度**。 ```java /** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ 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); } } ``` #### 2.2.3構造一個包含指定集合元素的列表,按照集合的迭代器返回它們的順序 ```java /** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. 構造一個包含指定集合元素的列表,按照集合的迭代器返回它們的順序 * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public ArrayList(Collection c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // defend against c.toArray (incorrectly) not returning Object[] //防禦c.toArray(錯誤地)不返回Object[] // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) // jdk bug(Arrays內部實現的ArrayList的toArray()方法的行為與規範不一致) 15年修復; if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } ``` #### JDK-6260652 `(see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)` 簡單來說就是,就是下圖程式碼會產生的情況。 ```java Object[] objects = new String[]{"string"}; objects[0] = 1; /** Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer */ ``` #### 產生原因 ```java public Object[] toArray() { return a.clone(); } ``` Arrays內部實現的ArrayList的toArray()方法的行為與規範不一致。根據JLS規範String[]的clone方法返回的也是String[]型別,所以toArray()方法返回的真實型別是String[],所以個toArray()[0]賦值時可能會導致型別不匹配的錯誤 jdk11中的Arrays內部實現的ArrayList的toArray()方法。所以呼叫copyOf()返回值型別為Object[] ```java public Object[] toArray() { return Arrays.copyOf(a, a.length, Object[].class); } ``` ### 2.3常見方法 #### 2.3.1插入 對於陣列(線性表)結構,插入操作分為兩種情況。一種是在元素序列尾部插入,另一種是在元素序列其他位置插入。ArrayList 的原始碼裡也體現了這兩種插入情況,如下: ```java /** * This helper method split out from add(E) to keep method * bytecode size under 35 (the -XX:MaxInlineSize default value), * which helps when add(E) is called in a C1-compiled loop. (這個輔助方法是從add(E)方法分離而來的,為了保持方法位元組碼低於35,這將有助於add(E)方法呼叫C1編譯迴圈) */ private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); /** private Object[] grow() { return grow(size + 1); }**/ //將新元素插入序列尾部 elementData[s] = e; size = s + 1; } /** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). 在此列表的指定位置插入指定的元素。將當前位於該位置的元素(如果有)和任何後續元素向右移動(在其索引中新增一個元素) * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); // 2. 將 index 及其之後的所有元素都向後移一位 System.arraycopy(elementData, index, elementData, index + 1, s - index); // 3. 將新元素插入至 index 處 elementData[index] = element; size = s + 1; } ``` ##### 2.3.1.1元素序列尾部插入 1. 檢測陣列是否有足夠的空間插入 2. 將新元素插入至序列尾部 如下圖: ![簡單插入](https://img2020.cnblogs.com/blog/1000117/202007/1000117-20200711141047885-1039767031.png) ##### 2.3.1.2元素序列指定位置(假設該位置合理)插入 1. 檢測陣列是否有足夠的空間 2. 將 index 及其之後的所有元素向後移一位 3. 將新元素插入至 index 處 如下圖: ![在指定位置插入](https://img2020.cnblogs.com/blog/1000117/202007/1000117-20200711141139291-1291931966.png) 從上圖可以看出,將新元素插入至序列指定位置,需要先將該位置及其之後的元素都向後移動一位,為新元素騰出位置。這個操作的時間`複雜度為O(N)`,頻繁移動元素可能會導致效率問題,特別是集合中元素數量較多時。在日常開發中,若非所需,我們應當儘量避免在大集合中呼叫第二個插入方法。 #### 2.3.2 ArrayList 的擴容機制 對於變長資料結構,當結構中沒有空餘空間可供使用時,就需要進行擴容。在 ArrayList 中,當空間用完,其會按照原陣列空間的1.5倍進行擴容。相關原始碼如下: ```java /** 擴容的入口方法 */ /** * Increases the capacity of this {@code ArrayList} instance, if * necessary, to ensure that it can hold at least the number of elements * specified by the minimum capacity argument. *增加{@code ArrayList}例項的容量,如果必需的,以確保它至少可以容納minimum capacity引數指定的元素數 * @param minCapacity the desired minimum capacity */ public void ensureCapacity(int minCapacity) { if (minCapacity > elementData.length && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA && minCapacity <= DEFAULT_CAPACITY)) { modCount++; grow(minCapacity); } } /** 擴容的核心方法 */ /** * Returns a capacity at least as large as the given minimum capacity. * Returns the current capacity increased by 50% if that suffices. * Will not return a capacity greater than MAX_ARRAY_SIZE unless * the given minimum capacity is greater than MAX_ARRAY_SIZE. 返回至少等於給定最小值的容量容量。返回當前容量增加50%,如果夠了。不會返回大於MAX_ARRAY_SIZE的容量,除非給定的最小容量大於MAX_ARRAY_SIZE * @param minCapacity the desired minimum capacity * @throws OutOfMemoryError if minCapacity is less than zero */ private int newCapacity(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); //舊容量大小+在舊容量基礎上增加50%(左移1位相當於除以2) 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(); // 如果最小容量超過 MAX_ARRAY_SIZE,則將陣列容量擴容至 Integer.MAX_VALUE return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } ``` #### 2.3.3刪除 不同於插入操作,ArrayList 沒有無參刪除方法。所以其只能刪除指定位置的元素或刪除指定元素,這樣就無法避免移動元素(除非從元素序列的尾部刪除)。相關程式碼如下: ```java /** 刪除指定位置的元素 */ /** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; // 返回被刪除的元素值 @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } /** 從列表中刪除第一個出現的指定元素(如果存在)。 如果列表不包含元素,則不變。更準確地說,刪除索引最低的元素 */ /** * Removes the first occurrence of the specified element from this list, * if it is present. If the list does not contain the element, it is * unchanged. More formally, removes the element with the lowest index * {@code i} such that * {@code Objects.equals(o, get(i))} * (if such an element exists). Returns {@code true} if this list * contained the specified element (or equivalently, if this list * changed as a result of the call). * * @param o element to be removed from this list, if present * @return {@code true} if this list contained the specified element */ public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; // 遍歷陣列,查詢要刪除元素的位置 found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; } /** * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) // 將 index + 1 及之後的元素向前移動一位,覆蓋被刪除值 System.arraycopy(es, i + 1, es, i, newSize - i); // 將最後一個元素置空,並將 size 值減1 es[size = newSize] = null; } ``` 上面的刪除方法並不複雜,這裡以第一個刪除方法為例,刪除一個元素步驟如下: 1. 獲取指定位置 index 處的元素值 2. 將 index + 1 及之後的元素向前移動一位 3. 將最後一個元素置空,並將 size 值減 1 4. 返回被刪除值,完成刪除操作 如下圖: ![刪除](https://img2020.cnblogs.com/blog/1000117/202007/1000117-20200711141231904-1946453737.png) 現在,考慮這樣一種情況。我們往 ArrayList 插入大量元素後,又刪除很多元素,此時底層陣列會空閒處大量的空間。因為 ArrayList 沒有自動縮容機制,導致底層陣列大量的空閒空間不能被釋放,造成浪費。對於這種情況,ArrayList 也提供了相應的處理方法,如下: ```java /** 將陣列容量縮小至元素數量 */ /** * Trims the capacity of this {@code ArrayList} instance to be the * list's current size. An application can use this operation to minimize * the storage of an {@code ArrayList} instance. */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } } ``` ![縮容](https://img2020.cnblogs.com/blog/1000117/202007/1000117-20200711141334553-573708431.png) 我們可以使用`trimToSize()`手動觸發 ArrayList 的縮容機制,釋放多餘的空間。 #### 2.3.4 遍歷 ArrayList 實現了 RandomAccess 介面(該介面是個標誌性介面),表明它具有`快速隨機訪問`的能力。ArrayList 底層基於陣列實現,所以它可在常數階的時間內完成隨機訪問,效率很高。對 ArrayList 進行遍歷時,一般情況下,我們喜歡使用 foreach 迴圈遍歷,但這並不是推薦的遍歷方式。ArrayList 具有隨機訪問的能力,如果在一些效率要求比較高的場景下,更推薦下面這種方式: ```java for (int i = 0; i < list.size(); i++) { list.get(i); } ``` 官網還特意說明了,如果是實現了這個介面的 **List**,那麼使用for迴圈的方式獲取資料會優於用迭代器獲取資料 ## 3.其他細節 ### 3.1 快速失敗機制 在 Java 集合框架中,很多類都實現了快速失敗機制。該機制被觸發時,會丟擲併發修改異常`ConcurrentModificationException`,這個異常大家在平時開發中多多少少應該都碰到過。關於快速失敗機制,ArrayList 的註釋裡對此做了解釋,這裡引用一下: > The iterators returned by this class’s iterator() and > listIterator(int) methods are fail-fast > if the list is structurally modified at any time after the iterator is > created, in any way except through the iterator’s own > ListIterator remove() or ListIterator add(Object) methods, > the iterator will throw a ConcurrentModificationException. Thus, in the face of > concurrent modification, the iterator fails quickly and cleanly, rather > than risking arbitrary, non-deterministic behavior at an undetermined > time in the future. 上面註釋大致意思是,ArrayList 迭代器中的方法都是均具有快速失敗的特性,當遇到併發修改的情況時,迭代器會快速失敗,以避免程式在將來不確定的時間裡出現不確定的行為。 以上就是 Java 集合框架中引入快速失敗機制的原因,並不難理解,這裡不多說了。 ### 3.2 關於遍歷時刪除 遍歷時刪除是一個不正確的操作,即使有時候程式碼不出現異常,但執行邏輯也會出現問題。關於這個問題,阿里巴巴 Java 開發手冊裡也有所提及。這裡引用一下: > 【強制】不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 物件加鎖。 相關程式碼(稍作修改)如下: ```j