概述
在前文:java集合原始碼分析(二):List與AbstractList 和 java集合原始碼分析(一):Collection 與 AbstractCollection 中,我們大致瞭解了從 Collection 介面到 List 介面,從 AbstractCollection 抽象類到 AbstractList 的層次關係和方法實現的大體過程。
在本篇文章,將在前文的基礎上,閱讀 List 最常用的實現類 Arraylist 的原始碼,深入瞭解這個“熟悉的陌生人”。
一、ArrayList 的類關係
ArrayList 實現了三個介面,繼承了一個抽象類,其中 Serializable ,Cloneable 與 RandomAccess 介面都是用於標記的空介面,他的主要抽象方法來自於 List,一些實現來自於 AbstractList。
1.AbstractList 與 List
ArrayList 實現了 List 介面,是 List 介面的實現類之一,他通過繼承抽象類 AbstractList 獲得的大部分方法的實現。
比較特別的是,理論上父類 AbstractList 已經實現類 AbstractList 介面,那麼理論上 ArrayList 就已經可以通過父類獲取 List 中的抽象方法了,不必再去實現 List 介面。
網上關於這個問題的答案眾說紛紜,有說是為了通過共同的介面便於實現 JDK 代理,也有說是為了程式碼規範性與可讀性的,在 Stack Overflow 上 Why does LinkedHashSet extend HashSet and implement Set 一個據說問過原作者的老哥給出了一個 it was a mistake
的回答,但是這似乎不足以解釋為什麼幾乎所有的容器類都有類似的行為。事實到底是怎麼回事,也許只有真正的原作者知道了。
2.RandomAccess
RandomAccess 是一個標記性的介面,實現了此介面的集合是允許被隨機訪問的。
根據 JavaDoc 的說法,如果一個類實現了此介面,那麼:
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),也根據這點做了一些處理。
3.Cloneable
Cloneable 介面表示它的實現類是可以被拷貝的,根據 JavaDoc 的說法:
一個類實現Cloneable介面,以表明該通過Object.clone()方法為該類的例項進行逐域複製是合法的。
在未實現Cloneable介面的例項上呼叫Object的clone方法會導致丟擲CloneNotSupportedException異常。
按照約定,實現此介面的類應使用公共方法重寫Object.clone()。
簡單的說,如果一個類想要使用Object.clone()
方法以實現物件的拷貝,那麼這個類需要實現 Cloneable 介面並且重寫 Object.clone()
方法。值得一提的是,Object.clone()
預設提供的拷貝是淺拷貝,淺拷貝實際上沒有拷貝並且建立一個新的例項,通過淺拷貝獲得的物件變數其實還是指標,指向的還是原來那個記憶體地址。深拷貝的方法需要我們自己提供。
4.Serializable
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;
我們來一個一個的解釋他們的作用。
1.serialVersionUID
private static final long serialVersionUID = 8683452581122892189L;
用於序列化檢測的 UUID,我們可以簡單的理解他的作用:
當序列化以後,serialVersionUID 會被一起寫入檔案,當反序列化的時候,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException。
更多內容仍然可以參考大佬的博文:java類中serialversionuid 作用 是什麼?舉個例子說明
2.DEFAULT_CAPACITY
預設容量,如果例項化的時候沒有在構造方法裡指定初始容量大小,第一個擴容就會根據這個值擴容。
3.EMPTY_ELEMENTDATA
一個空陣列,當呼叫構造方法的時候指定容量為0,或者其他什麼操作會導致集合內陣列長度變為0的時候,就會直接把空陣列賦給集合實際用於存放資料的陣列 elementData
。
4.DEFAULTCAPACITY_EMPTY_ELEMENTDATA
也是一個空陣列,不同於 EMPTY_ELEMENTDATA
是指定了容量為0的時候會被賦給elementData,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是在不指定容量的時候才會被賦給 elementData
,而且新增第一個元素的時候就會被擴容。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和 EMPTY_ELEMENTDATA
都不影響實際後續往裡頭新增元素,兩者主要表示一個邏輯上的區別:前者表示集合目前為空,但是以後可能會新增元素,而後者表示這個集合一開始就沒打算存任何東西,是個容量為0的空集合。
5.elementData
實際存放資料的陣列,當擴容或者其他什麼操作的時候,會先把資料拷貝到新陣列,然後讓這個變數指向新陣列。
6.size
集合中的元素數量(注意不是陣列長度)。
7.MAX_ARRAY_SIZE
允許的最大陣列長度,之所以等於 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 是如何實現擴容和縮容的。
0.System.arraycopy()
在這之前,我們需要理解一下擴容縮容所依賴的核心方法 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}
1.擴容
雖然在 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
,如果再大就只能溢位了。
以下是擴容的流程圖:
2.縮容
除了擴容,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);
}
}
3.測試
我們可以藉助反射,來看看 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
}
五、新增 / 獲取
1.add
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++;
}
新增的原理比較簡單,實際上就是如果不指定下標就插到陣列尾部,否則就先建立一個新陣列,然後把舊陣列的資料移動到新陣列,並且在這個過程中提前在新陣列上留好要插入的元素的空位,最後再把元素插入陣列。後面的增刪操作基本都是這個原理。
2.addAll
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;
}
3.get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// 根據下標從陣列中取值,被使用在get(),set(),remove()等方法中
E elementData(int index) {
return (E) elementData[index];
}
六、刪除 / 修改
1.remove
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
會直接拋異常。
2.clear
比較需要注意的是,相比起remove()
方法,clear()
只是把陣列的每一位都設定為null,elementData
的長度是沒有改變的:
public void clear() {
modCount++;
// 把陣列每一位都設定為null
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
3.removeAll / retainAll
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 {
// 1.遍歷本集合
for (; r < size; r++)
// 如果新增集合存在與本集合存在相同的元素,有兩種情況
// 1.removeAll,complement=false:直接跳過該元素
// 2.retainAll,complement=true:把新元素插入原集合頭部
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 2.如果上述操作中發生異常,則判斷是否已經完成本集合的遍歷
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 3.將陣列剩下的位置都改為null
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。
也就是說,在遍歷過程中如果不發生異常,就會跳過第二步,直接進入第三步。
當然,這是沒有發生異常的情況,因此遍歷完成後 r = size
,那麼如果遍歷到 r = 2
,也就是進入 if 分支後,程式發生了異常,尚未完成遍歷就進入了 finallly 塊,就會先進入第二步,也就是下面的流程:
最終陣列會變為 {C,C,D,null} ,只有最後一個 D 被刪除。
4.removeIf
這個是 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;
}
5.set
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
6.replaceAll
這也是一個 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++;
}
七、迭代
1.iterator / listIterator
ArrayList 重新實現了自己的迭代器,而不是繼續使用 AbstractList 提供的迭代器。
和 AbstracList 一樣,ArrayList 實現的迭代器內部類仍然是基礎迭代器 Itr 和加強的迭代器 ListItr,他和 AbstractList 中的兩個同名內部類基本一樣,但是針對 ArrayList 的特性對方法做了一些調整:比如一些地方取消了對內部方法的呼叫,直接對 elementData 下標進行操作等。
這一塊可以參考上篇文章,或者看看原始碼,這裡就不贅述了。
2.forEach
這是一個針對 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();
}
}
3.迭代刪除存在的問題
到目前為止,我們知道有三種迭代方式:
- 使用
iterator()
或listIterator()
獲取迭代器; forEach()
;- for 迴圈。
如果我們在迴圈中刪除集合的節點,只有迭代器的方式可以正常刪除,其他都會出問題。
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); // []
可見反向刪除是沒有問題的。
八、其他
1.indexOf / lastIndexOf / contains
相比起 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;
}
2.subList
subList()
和 iterator()
一樣,也是返回一個特殊的內部類 SubList,在 AbstractList 中也已經有相同的實現,只不過在 ArrayList 裡面進行了一些改進,大體邏輯和 AbstractList 中是相似的,這部分內容在前文已經有提到過,這裡就不再多費筆墨。
3.sort
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 表示式傳入一個匿名方法。
4.toArray
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;
}
5.clone
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,說明是同一個例項。
6.isEmpty
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對節點進行刪除,因為刪除時陣列“縮水”導致原本要刪除的下一下標對應的節點,卻落到了當前被刪除的節點對應的下標位置,導致被跳過。
如果從隊尾反向刪除,就不會引起陣列“縮水”,因此是正常的。