1. 程式人生 > >JDK原始碼分析系列-ArrayList

JDK原始碼分析系列-ArrayList

1、ArrayList本質

陣列 + 動態擴容實現的資料列表。

private static final Object[] EMPTY_ELEMENTDATA = {};

// elementData初始為空陣列 public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } // 指定初始容量,不能為負數 // 如果能預估集合大小,建議初始化時指定容量,避免擴容,提升效能 public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; }
初始為空陣列,故每次新增元素時進行擴容判斷,首次新增,預設初始化大小為10,見下文擴容內容。
private static final int DEFAULT_CAPACITY = 10;

2、主要屬性

private transient Object[] elementData ArrayList實際維護的底層陣列, 初始為空陣列,元素為物件引用。雖然該屬性是瞬態,但是ArrayList實現了Serializable介面,並內部自行實現了writeObjec和readObject序列化方法。見下文。
private int size elementData陣列中有效元素的個數,並非elementData陣列實際大小,擴容後使用null元素佔位,即:每次add均+1。size參與元素的按索引檢索。

3、主要特性

是否允許null元素   需要關注下原始碼中remove方法對null值的處理邏輯,見5
是否有序
是否執行緒安全 否 (Vector是執行緒安全的集合類 或 Collections.synchronizedList包裝)
是否允許重複元素
按索引插入、刪除元素的效率低 這兩類方法均涉及到陣列的複製,陣列個數越多,執行效率越低
順序插入、檢索(get)元素效率高 1、順序插入只有觸發擴容時才會進行陣列複製,其餘場景均連續新增陣列元素即可; 2、按索引隨機訪問陣列元素的效率高,陣列在記憶體中連續的記憶體空間,cpu快取會讀入連續的記憶體空間,所以陣列按下標定址時都是在cpu快取中進行,效率較高; (題外:連結串列非連續儲存,故在記憶體中定址,效率不如陣列);

4、插入元素

// 將指定元素e加入到elementData陣列中
public boolean add(E e) { // 檢查是否需要擴容,如果需要則執行擴容操作 ensureCapacityInternal(size + 1); // Increments modCount!! // 將新元素賦值給底層陣列對應的size++的位置 elementData[size++] = e; return true; } // 將指定元素新增至指定索引的位置, // 1. index位置元素及其後所有有效元素(使用size計算)進行復制並從index+1處進行貼上; // 2. 將element設定為index位置的元素; public void add(int index, E element) { // 檢查index位置是否越界  rangeCheckForAdd(index); // 檢查是否需要擴容,如果需要則執行擴容操作 ensureCapacityInternal(size + 1); // 完成元素的拷貝移動, 如: // elementData = [A, B, C, null, null, null, null, null, null, null] // add(1, D) // elementData = [A, B, B, C, null, null, null, null, null, null] System.arraycopy(elementData, index, elementData, index + 1, size - index); // 將新元素賦值給底層陣列對應的索引位置 elementData = [A, D, B, C, null, null, null, null, null, null] elementData[index] = element; // 有效元素size+1 size++; }

5、刪除元素

// 刪除首次匹配到的指定元素
public boolean remove(Object o) { // 如果刪除的元素是null,則不能使用equal判斷,故此處進行分支邏輯處理 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) // 使用equals判斷物件內容是否相等 if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } // 刪除指定索引位置的元素 public E remove(int index) { // 檢查引數索引值是否越界  rangeCheck(index); // 修改次數自增 modCount++; // 獲取要刪除的元素 E oldValue = elementData(index); // 從index+1處開始複製並在index處進行貼上 // 如: elementData=[A, B, C, D] // 刪除index=1位置的元素B,呼叫arrayCopy方法後, elementData=[A, C, D, D] // 賦值最後一個元素為null,等待gc回收,則 elementData=[A, C, D, null] int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); // size自減,將最後一個元素賦值為null,用於gc該多餘元素 elementData[--size] = null; // clear to let GC do its work // 返回被刪除元素的引用 return oldValue; }

6、動態擴容

// 內部動態擴容實現
// minCapacity為最小容量,即容納有效元素所需的容量,add操作時minCapacity為size + 1/numNew
private void ensureCapacityInternal(int minCapacity) { // 首次add,底層資料為空陣列,則minCapacity為10 if (elementData == 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; // 計算新容量,實際容量 * 1.5,1.5為時間與空間的權衡,擴容太大,則浪費空間,擴容太小,則發生頻繁擴容,則耗費效能 int newCapacity = oldCapacity + (oldCapacity >> 1); // 新的容量比所需的容量小,即:現有容量擴容1.5倍後仍然不夠,則使用所需容量進行擴容,當首次新增元素時,新容量為0,所需容量為10,則新容量為10 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果新容量大於最大容量,則觸發hugeCapacity操作 // MAX_ARRAY_SIZE為Integer.MAX_VALUE - 8, -8為防止記憶體溢位,見原始碼MAX_ARRAY_SIZE常量註釋 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 陣列複製,根據新容量大小,進行擴容 elementData = Arrays.copyOf(elementData, newCapacity); } // java.util.Arrays中的copyOf實現 // 如:original = [0, 1, 2, ...., 9 ] 當前容量oldCapacity=10, 新容量newCapacity=10*1.5=15,則 // 1、建立一個容量為15的新陣列copy, elementData = [null, null, null, ...., null] // 2、將原陣列original元素複製到新陣列copy中,返回新陣列 copy = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, null, null, null] public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { // 開闢新陣列,大小為newCapacity T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 將已有陣列original元素複製到新陣列copy中 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } /** * 有些虛擬機器陣列物件中存在8個位元組的物件頭,所以此處-8目的為減少OOM的可能性 * 如果超過了MAX_ARRAY_SIZE,那麼擴容至Integer.MAX_VALUE */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); // 最大擴容Integer.MAX_VALUE,即:整數上限值 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }

7、序列化

雖然ArrayList實現了Serializable介面, 但elementData為瞬態的,作者不希望使用預設的序列化方法對elementData進行序列化。原因為elementData包含佔位元素,直接序列化後會導致序列化後的內容比較大,浪費空間及序列化的效率,所以ArrayList中重寫writeObjec和readObject方法,writeObjec實現了對elementData中有效元素進行序列化的過程,readObject為反序列化過程。

序列化:ObjectOutputStream.defaultWriteObject序列化非 transient內容 -> ArrayList的writeObject序列化transient的elementData。參考 ObjectOutputStream。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; // 序列化非瞬態內容  s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() // 序列化實際大小size  s.writeInt(size); // 按size序列化底層陣列元素 // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }

8、與Vector的比較

Vector的原始碼實現與ArrayList繼承關係一致,底層均基於陣列實現且初始預設長度均為10,實現方式基本相同,所以這裡不再另起文章進行原始碼分析,這裡對Vector與ArrayList的區別進行梳理:

1、Vector是執行緒安全,而ArrayList是非執行緒安全的;

Vector類中的關鍵方法使用了Synchronized進行修飾,多執行緒同步執行,保障安全性,自然也犧牲了效能。

public synchronized boolean add(E e) {
    modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }

2、Vector允許自定義指定擴容增長因子,預設擴容增量100%,ArrayList擴容增量固定為50%;

public Vector() {
    this(10);
}
public Vector(int initialCapacity) { this(initialCapacity, 0); } public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } public Vector(Collection<? extends E> c) { elementData = c.toArray(); elementCount = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, elementCount, Object[].class); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }

3、Vector的底層陣列不是瞬態的且序列化只重寫了writeObject方法;

// 未使用transient修飾
protected Object[] elementData;

// 重寫writeObject方法與ArrayList不同 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // 獲取緩衝中的持久化欄位物件 final java.io.ObjectOutputStream.PutField fields = s.putFields(); final Object[] data; // 同步將capacityIncrement、elementCount新增至持久化欄位物件中 //複製底層陣列 synchronized (this) { fields.put("capacityIncrement", capacityIncrement); fields.put("elementCount", elementCount); data = elementData.clone(); } // 將複製後的底層陣列新增至持久化欄位物件中 fields.put("elementData", data); // 將持久化欄位物件寫入流  s.writeFields(); }

4、Vector具有一些特有的方法

public synchronized E firstElement() { ... }
public synchronized E lastElement() { ... } public synchronized void removeElementAt(int index) { ... } public synchronized void insertElementAt(E obj, int index) { ... } public synchronized void addElement(E obj) { ... } .......

9、迭代器、fail-fast和fail-safe

Iterator(迭代器):容器類通過實現Iterable介面來定義獲取迭代器物件的方法,通過實現Iterator介面來定義一個迭代器。通過迭代器物件,使訪問容器的程式碼邏輯從容器實現程式碼中剝離,使用者只需要通過迭代器的操作即可對容器進行遍歷,無需瞭解容器的內部結構,消除了容器由於內部結構不同而導致遍歷方式的差異,即迭代器模式的思想。   fail-fast(快速失敗)   對於非併發容器當使用迭代器遍歷時,當前執行緒或另一個執行緒通過呼叫容器的方法如add/remove等改變了容器結構,那麼容器在迭代過程中會丟擲ConcurrentModificationException執行時異常而終止後續迭代過程的機制。   下面通過ArrayList的原始碼來分析Iterator和fail-fast,ArrayList整合AbstractList抽象類,AbstractList繼承AbstractCollection抽象類並實現了該抽象類的iterator方法:
// 操作計數
protected transient int modCount = 0; // 獲取一個迭代器物件 public Iterator<E> iterator() { return new Itr(); } //迭代器類,AbstractList抽象類中的私有內部類,實現了Iterator介面 private class Itr implements Iterator<E> { // 下一個元素的索引 int cursor = 0; // 上一個元素的索引,當呼叫remove方法時重置為-1 int lastRet = -1; // 迭代器例項化時將期待的modCount值初始化為當前的modCount值,用於快速失敗檢查 int expectedModCount = modCount; // 判斷是否存在下一個元素 public boolean hasNext() { // 下一個元素的索引不等於總的底層陣列長度,則說明還有下一個元素 return cursor != size(); } // 獲取下一個元素 public E next() { // 檢查底層陣列結構是否發生修改,即判斷迭代器初始化時備份的expectedModCount的值和當前的modCount是否相等 // 不等,則丟擲ConcurrentModificationException  checkForComodification(); try { // 將當前下一個元素的索引賦值給i int i = cursor; // 獲取下一個元素 E next = get(i); // 將當前下一個元素索引作為上一個元素索引 lastRet = i; // 重置cursor指向新的next索引 cursor = i + 1; // 返回next物件 return next; } catch (IndexOutOfBoundsException e) { // 如果陣列越界則優先判斷是否發生了併發修改,如果是優先丟擲ConcurrentModificationException  checkForComodification(); // 否則 丟擲NoSuchElementException throw new NoSuchElementException(); } } // 迭代器物件的刪除操作 public void remove() { // 迭代器物件未吊用過next()方法便呼叫remove方法的話,會丟擲IllegalStateException if (lastRet < 0) throw new IllegalStateException(); // 併發修改異常判斷  checkForComodification(); try { // 根據索引刪除元素 AbstractList.this.remove(lastRet); // 下一元素索引減1 if (lastRet < cursor) cursor--; //重置上一元素/當前元素索引 lastRet = -1; // 更新 expectedModCount,所以單執行緒迭代過程中,通過迭代器物件的remove方法刪除元素,不會導致併發修改異常 expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } // fail-fast檢查 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
通過原始碼可以發現: 1、迭代器呼叫next()方法時才會進行fail-fast檢查; 2、單執行緒場景,在迭代過程中通過呼叫容器物件自身的add/remove操作修改了modCount,那麼在下次next()方法呼叫時會發生ConcurrentModificationException,所以可以使用迭代器物件自身的remove方法進行刪除;
// 錯誤方式
List<String> list = new ArrayList<>(3);
list.add("a"); list.add("b"); list.add("c"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { // 第二次遍歷時呼叫next()方法時,會丟擲ConcurrentModificationException  iterator.next(); list.remove(0); } // 正確方式 while (iterator.hasNext()) { iterator.next(); iterator.remove(); }
3、多執行緒場景,因為迭代器物件是執行緒私有的,也就是expectedModCount是執行緒私有的,執行緒一迭代遍歷,執行緒二呼叫了迭代器的remove修改了容器物件的modCount,那麼進行迭代遍歷的執行緒一也會發生ConcurrentModificationException,所以可以使用CopyOnWriteArrayList容器,詳見《CopyOnWriteArrayList原始碼分析》;   fail-safe(安全失敗)   CopyOnWriteArrayList等JUC包下的容器都具有安全失敗的特性,在迭代器初始化時快照容器內容,迭代遍歷的是容器的內容的快照,所以其他執行緒對容器結構的修改不會被迭代器感知到,也就不會丟擲ConcurrentModificationException。以CopyOnWriteArrayList原始碼為例:
// 迭代器物件獲取方法
public Iterator<E> iterator() {
    // 獲取當前陣列從索引為0的位置例項化一個迭代器物件 return new COWIterator<E>(getArray(), 0); } private static class COWIterator<E> implements ListIterator<E> { // 迭代器內部對原陣列的引用進行了快照(淺拷貝),final修飾,建構函式中初始化,迭代器遍歷操作針對該快照完成 private final Object[] snapshot; /** Index of element to be returned by subsequent call to next. */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } ..... // 該迭代器與ArrayList不同,不支援remove方法 public void remove() { throw new UnsupportedOperationException(); } // 不支援通過迭代器進行set public void set(E e) { throw new UnsupportedOperationException(); } // 不支援通過迭代器進行add public void add(E e) { throw new UnsupportedOperationException(); } }