在前文:java集合源碼分析(二):List與AbstractList 和 java集合源碼分析(一):Collection 與 AbstractCollection 中,咱們大體瞭解了從 Collection 接口到 List 接口,從 AbstractCollection 抽象類到 AbstractList 的層次關係和方法實現的大致過程。html
在本篇文章,將在前文的基礎上,閱讀 List 最經常使用的實現類 Arraylist 的源碼,深刻了解這個「熟悉的陌生人」。java
ArrayList 實現了三個接口,繼承了一個抽象類,其中 Serializable ,Cloneable 與 RandomAccess 接口都是用於標記的空接口,他的主要抽象方法來自於 List,一些實現來自於 AbstractList。算法
ArrayList 實現了 List 接口,是 List 接口的實現類之一,他經過繼承抽象類 AbstractList 得到的大部分方法的實現。數組
比較特別的是,理論上父類 AbstractList 已經實現類 AbstractList 接口,那麼理論上 ArrayList 就已經能夠經過父類獲取 List 中的抽象方法了,沒必要再去實現 List 接口。網絡
網上關於這個問題的答案衆說紛紜,有說是爲了經過共同的接口便於實現 JDK 代理,也有說是爲了代碼規範性與可讀性的,在 Stack Overflow 上 Why does LinkedHashSet extend HashSet and implement Set 一個聽說問過原做者的老哥給出了一個 it was a mistake
的回答,可是這彷佛不足以解釋爲何幾乎全部的容器類都有相似的行爲。事實究竟是怎麼回事,也許只有真正的原做者知道了。併發
RandomAccess 是一個標記性的接口,實現了此接口的集合是容許被隨機訪問的。app
根據 JavaDoc 的說法,若是一個類實現了此接口,那麼:dom
for (int i=0, n=list.size(); i < n; i++) list.get(i);
要快於函數
for (Iterator i=list.iterator(); i.hasNext(); ) i.next();
隨機訪問其實就是根據下標訪問,以 LinkedList 和 ArrayList 爲例,LinkedList 底層實現是鏈表,隨機訪問須要遍歷鏈表,複雜度爲 O(n),而 ArrayList 底層實現爲數組,隨機訪問直接經過下標去尋址就好了,複雜度是O(1)。源碼分析
當咱們須要指定迭代的算法的時候,能夠經過實現類是否實現了 RandomAccess 接口來選擇對應的迭代方式。在一些方法操做集合的方法裏(好比 AbstractList 中的 subList),也根據這點作了一些處理。
Cloneable 接口表示它的實現類是能夠被拷貝的,根據 JavaDoc 的說法:
一個類實現Cloneable接口,以代表該經過Object.clone()方法爲該類的實例進行逐域複製是合法的。
在未實現Cloneable接口的實例上調用Object的clone方法會致使拋出CloneNotSupportedException異常。
按照約定,實現此接口的類應使用公共方法重寫Object.clone()。
簡單的說,若是一個類想要使用Object.clone()
方法以實現對象的拷貝,那麼這個類須要實現 Cloneable 接口而且重寫 Object.clone()
方法。值得一提的是,Object.clone()
默認提供的拷貝是淺拷貝,淺拷貝實際上沒有拷貝而且建立一個新的實例,經過淺拷貝得到的對象變量其實仍是指針,指向的仍是原來那個內存地址。深拷貝的方法須要咱們本身提供。
Serializable 接口也是一個標記性接口,他代表實現類是能夠被序列化與反序列化的。
這裏提一下序列化的概念。
序列化是指把一個 Java 對象變成二進制內容的過程,本質上就是把對象轉爲一個 byte[] 數組,反序列化同理。
當一個 java 對象序列化之後,就能夠獲得的 byte[] 保存到文件中,或者把 byte[] 經過網絡傳輸到遠程,這樣就至關於把 Java 對象存儲到文件或者經過網絡傳輸出去了。
值得一提的是,針對一些不但願被存儲到文件,或者以字節流的形式被傳輸的私密信息,java 提供了 transient 關鍵字,被其標記的屬性不會被序列化。好比在 AbstractList 裏,以前提到的併發修改檢查中用於記錄結構性操做次數的變量 modCount
,還有下面要介紹到的 ArrayList 的底層數組 elementData 就是被 transient 關鍵字修飾的。
更多的內容能夠參考大佬的博文:Java transient關鍵字使用小記
在 ArrayList 中,一共有七個成員變量:
private static final long serialVersionUID = 8683452581122892189L; /** * 默認初始容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 用於空實例的共享空數組實例 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 共享的空數組實例,用於默認大小的空實例。咱們將此與EMPTY_ELEMENTDATA區別開來,以瞭解添加第一個元素時要擴容數組到多大。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 存儲ArrayList的元素的數組緩衝區。 ArrayList的容量是此數組緩衝區的長度。添加第一個元素時,任何符合 * elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空ArrayList都將擴展爲DEFAULT_CAPACITY。 */ transient Object[] elementData; /** * ArrayList的大小(它包含的元素數) */ private int size; /** * 要分配的最大數組大小 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
咱們來一個一個的解釋他們的做用。
private static final long serialVersionUID = 8683452581122892189L;
用於序列化檢測的 UUID,咱們能夠簡單的理解他的做用:
當序列化之後,serialVersionUID 會被一塊兒寫入文件,當反序列化的時候,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常,便是InvalidCastException。
更多內容仍然能夠參考大佬的博文:java類中serialversionuid 做用 是什麼?舉個例子說明
默認容量,若是實例化的時候沒有在構造方法裏指定初始容量大小,第一個擴容就會根據這個值擴容。
一個空數組,當調用構造方法的時候指定容量爲0,或者其餘什麼操做會致使集合內數組長度變爲0的時候,就會直接把空數組賦給集合實際用於存放數據的數組 elementData
。
也是一個空數組,不一樣於 EMPTY_ELEMENTDATA
是指定了容量爲0的時候會被賦給elementData,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是在不指定容量的時候纔會被賦給 elementData
,並且添加第一個元素的時候就會被擴容。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和 EMPTY_ELEMENTDATA
都不影響實際後續往裏頭添加元素,二者主要表示一個邏輯上的區別:前者表示集合目前爲空,可是之後可能會添加元素,然後者表示這個集合一開始就沒打算存任何東西,是個容量爲0的空集合。
實際存放數據的數組,當擴容或者其餘什麼操做的時候,會先把數據拷貝到新數組,而後讓這個變量指向新數組。
集合中的元素數量(注意不是數組長度)。
容許的最大數組長度,之因此等於 Integer.MAX_VALUE - 8
,是爲了防止在一些虛擬機中數組頭會被用於保持一些其餘信息。
ArrayList 中提供了三個構造方法:
ArrayList()
ArrayList(int initialCapacity)
ArrayList(Collection<? extends E> c)
// 1.構造一個空集合 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 2.構造一個具備指定初始容量的空集合 public ArrayList(int initialCapacity) { // 判斷指定的初始容量是否大於0 if (initialCapacity > 0) { // 若大於0,則直接指定elementData數組的長度 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 若等於0,將EMPTY_ELEMENTDATA賦給elementData this.elementData = EMPTY_ELEMENTDATA; } else { // 小於0,拋異常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } } // 3.構造一個包含指定集合全部元素的集合 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); // 判斷傳入的集合是否爲空集合 if ((size = elementData.length) != 0) { // 確認轉爲的集合底層實現是否也是Objcet數組 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 若是是空集合,將EMPTY_ELEMENTDATA賦給elementData this.elementData = EMPTY_ELEMENTDATA; } }
咱們通常使用比較多的是第一種,有時候會用第三種,實際上,若是咱們能夠估計到實際會添加多少元素,就可使用第二種構造器指定容量,避免擴容帶來的消耗。
ArrayList 的可擴展性是它最重要的特性之一,在開始瞭解其餘方法前,咱們須要先了解一下 ArrayList 是如何實現擴容和縮容的。
在這以前,咱們須要理解一下擴容縮容所依賴的核心方法 System.arraycopy()
方法:
/** * 從一個源數組複製元素到另外一個數組,若是該數組指定位置已經有元素,就使用複製過來的元素替換它 * * @param src 要複製的源數組 * @param srcPos 要從源數組哪一個下標開始複製 * @param dest 要被移入元素的數組 * @param destPos 要從被移入元素數組哪一個下標開始替換 * @param length 複製元素的個數 */ arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
咱們舉個例子,假如咱們如今有 arr1 = {1,2,3,4,5}
和 arr2 = {6,7,8,9,10}
,如今咱們使用 arraycopy(arr1, 0, arr2, 0, 2)
,則意爲:
使用從 arr1 索引爲 0 的元素開始,複製 2 個元素,而後把這兩個元素從 arr2 數組中索引爲 0 的地方開始替換本來的元素,
int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = {6, 7, 8, 9, 10}; System.arraycopy(arr1, 0, arr2, 0, 2); // arr2 = {1,2,8,9,10}
雖然在 AbstractCollection 抽象類中已經有了簡單的擴容方法 finishToArray()
,可是 ArrayList 沒有繼續使用它,而是本身從新實現了擴容的過程。ArrayList 的擴容過程通常發生在新增元素上。
咱們以 add()
方法爲例:
public boolean add(E e) { // 判斷新元素加入後,集合是否須要擴容 ensureCapacityInternal(size + 1); elementData[size++] = e; return true; }
(1)檢查是否初次擴容
咱們知道,在使用構造函數構建集合的時候,若是未指定初始容量,則內部數組 elementData
會被賦上默認空數組 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
。
所以,當咱們調用 add()
時,會先調用 ensureCapacityInternal()
方法判斷elementData
是否仍是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,若是是,說明建立的時候沒有指定初始容量,並且沒有被擴容過,所以要保證集合被擴容到10或者更大的容量:
private void ensureCapacityInternal(int minCapacity) { // 判斷是否仍是初始狀態 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 擴容到默認容量(10)或更大 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
(2)檢查是否須要擴容
當決定好了第一次擴容的大小,或者elementData
被擴容過最少一次之後,就會進入到擴容的準備過程ensureExplicitCapacity()
,在這個方法中,將會增長操做計數器modCount
,而且保證新容量要比當前數組長度大:
private void ensureExplicitCapacity(int minCapacity) { // 擴容也是結構性操做,modCount+1 modCount++; // 判斷最小所需容量是否大於當前底層數組長度 if (minCapacity - elementData.length > 0) grow(minCapacity); }
(3)擴容
最後進入真正的擴容方法 grow()
:
// 擴容 private void grow(int minCapacity) { // 舊容量爲數組當前長度 int oldCapacity = elementData.length; // 新容量爲舊容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 若是新容量小於最小所需容量(size + 1),就以最小所需容量做爲新容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 若是新容量大於容許的最大容量,就再判斷可否再繼續擴容 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 擴容完畢,將舊數組的數據拷貝到新數組上 elementData = Arrays.copyOf(elementData, newCapacity); }
這裏可能有人會有疑問,爲何oldCapacity
要等於elementData.length
而不能夠是 size()
呢?
由於在 ArrayList,既有須要完全移除元素並新建數組的真刪除,也有隻是對應下標元素設置爲 null 的假刪除,size()
實際計算的是有元素個數,所以這裏須要使用elementData.length
來了解數組的真實長度。
回到擴容,因爲 MAX_ARRAY_SIZE
已是理論上容許的最大擴容大小了,若是新容量比MAX_ARRAY_SIZE
還大,那麼就涉及到一個臨界擴容大小的問題,hugeCapacity()
方法被用於決定最終容許的容量大小:
private static int hugeCapacity(int minCapacity) { // 是否發生溢出 if (minCapacity < 0) // overflow throw new OutOfMemoryError ("Required array size too large"); // 判斷最終大小是MAX_ARRAY_SIZE仍是Integer.MAX_VALUE return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
ArrayList 的 hugeCapacity()
與 AbstractCollection
抽象類中的 hugeCapacity()
是徹底同樣的,當 minCapacity > MAX_ARRAY_SIZE
的狀況成立的時候,說明如今的當前元素個數size
容量已經等於 MAX_ARRAY_SIZE
,數組已經極大了,這個時候再進行拷貝操做會很是消耗性能,所以最後一次擴容會直接擴到 Integer.MAX_VALUE
,若是再大就只能溢出了。
如下是擴容的流程圖:
除了擴容,ArrayList 還提供了縮容的方法 trimToSize()
,可是這個方法不被任何其餘內部方法調用,只能由程序猿本身去調用,主動讓 ArrayList 瘦身,所以在平常使用中並非很常見。
public void trimToSize() { // 結構性操做,modCount+1 modCount++; // 判斷當前元素個數是否小於當前底層數組的長度 if (size < elementData.length) { // 若是長度爲0,就變爲EMPTY_ELEMENTDATA空數組 elementData = (size == 0) ? EMPTY_ELEMENTDATA // 不然就把容量縮小爲當前的元素個數 : Arrays.copyOf(elementData, size); } }
咱們能夠藉助反射,來看看 ArrayList 的擴容和縮容過程:
先寫一個經過反射獲取 elementData 的方法:
// 經過反射獲取值 public static void getEleSize(List<?> list) { try { Field ele = list.getClass().getDeclaredField("elementData"); ele.setAccessible(true); Object[] arr = (Object[]) ele.get(list); System.out.println("當前elementData數組的長度:" + arr.length); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }
而後實驗看看:
public static void main(String[] args) { // 第一次擴容 ArrayList<String> list = new ArrayList<>(); getEleSize(list); // 當前elementData數組的長度:0 list.add("aaa"); getEleSize(list); // 當前elementData數組的長度:10 // 指定初始容量爲0的集合,進行第一次擴容 ArrayList<String> emptyList = new ArrayList<>(0); getEleSize(emptyList); // 當前elementData數組的長度:0 emptyList.add("aaa"); getEleSize(emptyList); // 當前elementData數組的長度:1 // 擴容1.5倍 for (int i = 0; i < 10; i++) { list.add("aaa"); } getEleSize(list); // 當前elementData數組的長度:15 // 縮容 list.trimToSize(); getEleSize(list);// 當前elementData數組的長度:11 }
public boolean add(E e) { // 若是須要就先擴容 ensureCapacityInternal(size + 1); // 添加到當前位置的下一位 elementData[size++] = e; return true; } public void add(int index, E element) { // 若 index > size || index < 0 則拋 IndexOutOfBoundsException 異常 rangeCheckForAdd(index); // 若是須要就先擴容 ensureCapacityInternal(size + 1); // 把本來 index 下標之後的元素集體後移一位,爲新插入的數組騰位置 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
添加的原理比較簡單,實際上就是若是不指定下標就插到數組尾部,不然就先建立一個新數組,而後把舊數組的數據移動到新數組,而且在這個過程當中提早在新數組上留好要插入的元素的空位,最後再把元素插入數組。後面的增刪操做基本都是這個原理。
public boolean addAll(Collection<? extends E> c) { // 將新集合的數組取出 Object[] a = c.toArray(); int numNew = a.length; // 若有必要就擴容 ensureCapacityInternal(size + numNew); // 將新數組拼接到原數組的尾部 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; // 先擴容 ensureCapacityInternal(size + numNew); // 判斷是否須要移動原數組 int numMoved = size - index; if (numMoved > 0) // 則將本來 index 下標之後的元素移到 index + numNew 的位置 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
public E get(int index) { rangeCheck(index); return elementData(index); } // 根據下標從數組中取值,被使用在get(),set(),remove()等方法中 E elementData(int index) { return (E) elementData[index]; }
public E remove(int index) { // 若 index >= size 會拋出 IndexOutOfBoundsException 異常 rangeCheck(index); modCount++; E oldValue = elementData(index); // 判斷是否須要移動數組 int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); // 把元素尾部位置設置爲null,便於下一次插入 elementData[--size] = null; return oldValue; } public boolean remove(Object o) { // 若是要刪除的元素是null if (o == null) { for (int index = 0; index < size; index++) // 移除第一位爲null的元素 if (elementData[index] == null) { fastRemove(index); return true; } } else { // 若是要刪除的元素不爲null for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
這裏有用到一個fastRemove()
方法:
// fast 的地方在於:跳過邊界檢查,而且不返回刪除的值 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; }
比較有趣的地方在於,remove()
的時候檢查的是index >= size
,而 add()
的時候檢查的是 index > size || index < 0
,可見添加的時候還要看看 index 是否小於0。
緣由在於 add()
在校驗完之後,馬上就會調用System.arraycopy()
,因爲這是個 native 方法,因此出錯不會拋異常;而 remve()
調用完後,會先使用 elementData(index)
取值,這時若是 index<0
會直接拋異常。
比較須要注意的是,相比起remove()
方法,clear()
只是把數組的每一位都設置爲null,elementData
的長度是沒有改變的:
public void clear() { modCount++; // 把數組每一位都設置爲null for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); } public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true); }
這兩個方法都依賴於 batchRemove()
方法:
private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { // 遍歷本集合 for (; r < size; r++) // 若是新增集合存在與本集合存在相同的元素,有兩種狀況 // 1.removeAll,complement=false:直接跳過該元素 // 2.retainAll,complement=true:把新元素插入原集合頭部 if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // 若是上述操做中發生異常,則判斷是否已經完成本集合的遍歷 if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
上述過程可能有點難一點理解,咱們假設這是 retailAll()
,所以 complement=true
,執行流程是這樣的:
同理,若是是removeAll()
,那麼 w 就會始終爲0,最後就會把 elementData 的全部位置都設置爲 null。
這個是 JDK8 之後的新增方法:
public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); int removeCount = 0; final BitSet removeSet = new BitSet(size); final int expectedModCount = modCount; final int size = this.size; // 遍歷集合,同時作併發修改檢查 for (int i=0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; // 使用 lambda 表達式傳入的匿名方法校驗元素 if (filter.test(element)) { removeSet.set(i); removeCount++; } } // 併發修改檢測 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // 是否有有須要刪除的元素 final boolean anyToRemove = removeCount > 0; if (anyToRemove) { // 新容量爲舊容量-刪除元素數量 final int newSize = size - removeCount; // 把被刪除的元素留下的空位「補齊」 for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) { i = removeSet.nextClearBit(i); elementData[j] = elementData[i]; } // 將刪除的位置設置爲null for (int k=newSize; k < size; k++) { elementData[k] = null; } this.size = newSize; if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove; }
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
這也是一個 JDK8 新增的方法:
public void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final int expectedModCount = modCount; final int size = this.size; // 遍歷,並使用lambda表達式傳入的匿名函數處理每個元素 for (int i=0; modCount == expectedModCount && i < size; i++) { elementData[i] = operator.apply((E) elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; }
ArrayList 從新實現了本身的迭代器,而不是繼續使用 AbstractList 提供的迭代器。
和 AbstracList 同樣,ArrayList 實現的迭代器內部類仍然是基礎迭代器 Itr 和增強的迭代器 ListItr,他和 AbstractList 中的兩個同名內部類基本同樣,可是針對 ArrayList 的特性對方法作了一些調整:好比一些地方取消了對內部方法的調用,直接對 elementData 下標進行操做等。
這一塊能夠參考上篇文章,或者看看源碼,這裏就不贅述了。
這是一個針對 Collection 的父接口 Iterable 接口中 forEach 方法的重寫。在 ArrayList 的實現是這樣的:
public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); // 獲取 modCount final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { // 遍歷元素並調用lambda表達式處理元素 action.accept(elementData[i]); } // 遍歷結束後才進行併發修改檢測 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
到目前爲止,咱們知道有三種迭代方式:
iterator()
或listIterator()
獲取迭代器;forEach()
;若是咱們在循環中刪除集合的節點,只有迭代器的方式能夠正常刪除,其餘都會出問題。
forEach
咱們先試試使用 forEach()
:
ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D")); arrayList1.forEach(arrayList1::remove); // java.util.ConcurrentModificationException
可見會拋出 ConcurrentModificationException
異常,咱們回到 forEach()
的代碼中:
public void forEach(Consumer<? super E> action) { // 獲取 modCount final int expectedModCount = modCount; ... ... for () { // 遍歷元素並調用lambda表達式處理元素 action.accept(elementData[i]); } ... ... // 遍歷結束後才進行併發修改檢測 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
因爲在方法執行的開始就令 expectedModCount= modCount
,等到循環處理結束後才進行 modCount != expectedModCount
的判斷,這樣若是咱們在匿名函數中對元素作了一些結構性操做,致使 modCount
增長,最後就會在檢測就會發現循環結束之後的 modCount
與一開始獲得的 modCount
不一致,因此會拋出 ConcurrentModificationException
異常。
for循環
先寫一個例子:
ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D")); for (int i = 0; i < arrayList1.size(); i++) { arrayList1.remove(i); } System.out.println(arrayList1); // [B, D]
能夠看到,B 和 C 的刪除被跳過了。實際上,這個問題和 AbstractList 的迭代器 Itr 中 remove()
方法遇到的問題有點像:
在 AbstractList 的 Itr 中,每次刪除都會致使數組的「縮短」,在被刪除元素的前一個元素會在 remove()
後「補空」,落到被刪除元素下標所對應的位置上,也就是說,假若有 a,b 兩個元素,刪除了下標爲0的元素a之後,b就會落到下標爲0的位置。
上文提到 ArrayList 的 remove()
調用了 fastRemove()
方法,咱們能夠看看他是否就是罪魁禍首:
private void fastRemove(int index) { ... ... // 若是不是在數組末尾刪除 if (numMoved > 0) // 數組被縮短了 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; }
因此數組「縮短」致使的元素下標變更就是問題的根源,換句話說,若是不調用 System.arraycopy()
方法,理論上就不會引發這個問題,因此咱們能夠試試反向刪除:
ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D")); // 反向刪除 for (int i = arrayList1.size() - 1; i >= 0; i--) { arrayList1.remove(i); } System.out.println(arrayList1); // []
可見反向刪除是沒有問題的。
相比起 AbstractList ,ArrayList 再也不使用迭代器,而是改寫成了根據下標進行for循環:
// indexOf 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 public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; }
至於 contains()
方法,因爲已經實現了 indexOf()
,天然沒必要繼續使用 AbstractCollection 提供的迭代查找了,而是改爲了:
public boolean contains(Object o) { return indexOf(o) >= 0; }
subList()
和 iterator()
同樣,也是返回一個特殊的內部類 SubList,在 AbstractList 中也已經有相同的實現,只不過在 ArrayList 裏面進行了一些改進,大致邏輯和 AbstractList 中是類似的,這部份內容在前文已經有提到過,這裏就再也不多費筆墨。
public void sort(Comparator<? super E> c) { final int expectedModCount = modCount; Arrays.sort((E[]) elementData, 0, size, c); if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; }
java 中集合排序要麼元素類實現 Comparable 接口,要麼本身寫一個 Comparator 比較器。這個函數的參數指明瞭類型是比較器,所以只能傳遞自定義的比較器,在 JDK8 之後,Comparator 類提供的了一些默認實現,咱們能夠以相似 Comparator.reverseOrder()
的方式去調用,或者直接用 lambda 表達式傳入一個匿名方法。
toArray()
方法在 AbstractList 的父類 AbstractCollection 中已經有過基本的實現,ArrayList 根據本身的狀況重寫了該方法:
public Object[] toArray() { // 直接返回 elementData 的拷貝 return Arrays.copyOf(elementData, size); } public <T> T[] toArray(T[] a) { // 若是傳入的素組比本集合的元素數量少 if (a.length < size) // 直接返回elementData的拷貝 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 把elementData的0到size的元素覆蓋到傳入數組 System.arraycopy(elementData, 0, a, 0, size); // 若是傳入數組元素比本集合的元素多 if (a.length > size) // 讓傳入數組size位置變爲null a[size] = null; return a; }
ArrayList 實現了 Cloneable 接口,所以他理當有本身的 clone()
方法:
public Object clone() { try { // Object.clone()拷貝ArrayList ArrayList<?> v = (ArrayList<?>) super.clone(); // 拷貝 v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
要注意的是,經過 clone()
獲得的 ArrayList 不是同一個實例,可是使用 Arrays.copyOf()
獲得的元素對象是同一個對象。咱們舉個例子:
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { ArrayList<MyBean> arrayList1 = new ArrayList<>(Arrays.asList(new MyBean())); ArrayList<MyBean> arrayList2 = (ArrayList<MyBean>) arrayList1.clone(); System.out.println(arrayList1); // [$MyBean@782830e] System.out.println(arrayList2); // [$MyBean@782830e] System.out.println(arrayList1 == arrayList2); // false arrayList1.add(new MyBean()); System.out.println(arrayList1); // [MyBean@782830e, $MyBean@470e2030] arrayList2.add(new MyBean()); System.out.println(arrayList2); // [$MyBean@782830e, $MyBean@3fb4f649] } public static class MyBean {}
能夠看到,arrayList1 == arrayList2
是 false,說明是 ArrayList 兩個實例,可是內部的第一個 MyBean 都是 $MyBean@782830e,說明是同一個實例。
public boolean isEmpty() { return size == 0; }
ArrayList 底層是 Object[] 數組,被 RandomAccess 接口標記,具備根據下標高速隨機訪問的功能;
ArrayList 擴容是擴大1.5倍,只有構造方法指定初始容量爲0時,纔會在第一次擴容出現小於10的容量,不然第一次擴容後的容量必然大於等於10;
ArrayList 有縮容方法trimToSize()
,可是自身不會主動調用。當調用後,容量會縮回實際元素數量,最小會縮容至默認容量10;
ArrayList 的添加可能會由於擴容致使數組「膨脹」,同理,不是全部的刪除都會引發數組「縮水」:當刪除的元素是隊尾元素,或者clear()
方法都只會把下標對應的地方設置爲null,而不會真正的刪除數組這個位置;
ArrayList 在循環中刪除——準確的講,是任何會引發 modCount
變化的結構性操做——可能會引發意外:
在 forEach()
刪除元素會拋ConcurrentModificationException
異常,由於 forEach()
在循環開始前就獲取了 modCount
,可是到循環結束才比較舊 modCount
和最新的 modeCount
;
在 for 循環裏刪除其實是以步長爲2對節點進行刪除,由於刪除時數組「縮水」致使本來要刪除的下一下標對應的節點,卻落到了當前被刪除的節點對應的下標位置,致使被跳過。
若是從隊尾反向刪除,就不會引發數組「縮水」,所以是正常的。