1.概述數組
ArrayList
是一種變長的集合類, ,當往 ArrayList 中添加的元素數量大於其底層數組容量時,其會經過擴容機制從新生成一個更大的數組。另外,因爲 ArrayList 底層基於數組實現,因此其能夠保證在 O(1)
複雜度下完成隨機查找操做。其餘方面,ArrayList 是非線程安全類,併發環境下,多個線程同時操做 ArrayList,會引起不可預知的錯誤。安全
ArrayList 是你們最爲經常使用的集合類,做爲一個變長集合類,其核心是擴容機制。因此只要知道它是怎麼擴容的,以及基本的操做是怎樣實現就夠了。本文後續內容也將圍繞這些點展開敘述。數據結構
2.源碼分析併發
ArrayList 有兩個構造方法,一個是無參,另外一個需傳入初始容量值。你們平時最經常使用的是無參構造方法,相關代碼以下:框架
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; 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); } } public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
上面的代碼比較簡單,兩個構造方法作的事情並不複雜,目的都是初始化底層數組 elementData。區別在於無參構造方法會將 elementData 初始化一個空數組,插入元素時,擴容將會按默認值從新初始化數組。而有參的構造方法則會將 elementData 初始化爲參數值大小(>= 0)的數組。通常狀況下,咱們用默認的構造方法便可。假若在可知道將會向 ArrayList 插入多少元素的狀況下,應該使用有參構造方法。按需分配,避免浪費。dom
對於數組(線性表)結構,插入操做分爲兩種狀況。一種是在元素序列尾部插入,另外一種是在元素序列其餘位置插入。ArrayList 的源碼裏也體現了這兩種插入狀況,以下:源碼分析
對於在元素序列尾部插入,這種狀況比較簡單,只需兩個步驟便可:ui
以下圖:this
若是是在元素序列指定位置(假設該位置合理)插入,則狀況稍微複雜一點,須要三個步驟:spa
以下圖:
從上圖能夠看出,將新元素插入至序列指定位置,須要先將該位置及其以後的元素都向後移動一位,爲新元素騰出位置。這個操做的時間複雜度爲O(N)
,頻繁移動元素可能會致使效率問題,特別是集合中元素數量較多時。在平常開發中,若非所需,咱們應當儘可能避免在大集合中調用第二個插入方法。
以上是 ArrayList 插入相關的分析,上面的分析以及配圖均未體現擴容機制。那麼下面就來簡單分析一下 ArrayList 的擴容機制。對於變長數據結構,當結構中沒有空餘空間可供使用時,就須要進行擴容。在 ArrayList 中,當空間用完,其會按照原數組空間的1.5倍進行擴容。相關源碼以下:
/** 計算最小容量 */ private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } /** 擴容的入口方法 */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } /** 擴容的核心方法 */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 擴容 elementData = Arrays.copyOf(elementData, newCapacity); } 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; }
上面就是擴容的邏輯,代碼雖多,但不少都是邊界檢查,這裏就不詳細分析了。
不一樣於插入操做,ArrayList 沒有無參刪除方法。因此其只能刪除指定位置的元素或刪除指定元素,這樣就沒法避免移動元素(除非從元素序列的尾部刪除)。相關代碼以下:
/** 刪除指定位置的元素 */ public E remove(int index) { rangeCheck(index); modCount++; // 返回被刪除的元素值 E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 將 index + 1 及以後的元素向前移動一位,覆蓋被刪除值 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 將最後一個元素置空,並將 size 值減1 elementData[--size] = null; // clear to let GC do its work return oldValue; } E elementData(int index) { return (E) elementData[index]; } /** 刪除指定元素,若元素重複,則只刪除下標最小的元素 */ public boolean remove(Object o) { 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++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /** 快速刪除,不作邊界檢查,也不返回刪除的元素值 */ 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; // clear to let GC do its work }
上面的刪除方法並不複雜,這裏以第一個刪除方法爲例,刪除一個元素步驟以下:
以下圖:
上面就是刪除指定位置元素的分析,並非很複雜。
如今,考慮這樣一種狀況。咱們往 ArrayList 插入大量元素後,又刪除不少元素,此時底層數組會空閒處大量的空間。由於 ArrayList 沒有自動縮容機制,致使底層數組大量的空閒空間不能被釋放,形成浪費。對於這種狀況,ArrayList 也提供了相應的處理方法,以下:
/** 將數組容量縮小至元素數量 */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
經過上面的方法,咱們能夠手動觸發 ArrayList 的縮容機制。這樣就能夠釋放多餘的空間,提升空間利用率。
ArrayList 實現了 RandomAccess 接口(該接口是個標誌性接口),代表它具備隨機訪問的能力。ArrayList 底層基於數組實現,因此它可在常數階的時間內完成隨機訪問,效率很高。對 ArrayList 進行遍歷時,通常狀況下,咱們喜歡使用 foreach 循環遍歷,但這並非推薦的遍歷方式。ArrayList 具備隨機訪問的能力,若是在一些效率要求比較高的場景下,更推薦下面這種方式:
for (int i = 0; i < list.size(); i++) { list.get(i); }
至於緣由也不難理解,foreach 最終會被轉換成迭代器遍歷的形式,效率不如上面的遍歷方式。
3.其餘細節
在 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 集合框架中引入快速失敗機制的緣由,並不難理解,這裏很少說了。
遍歷時刪除是一個不正確的操做,即便有時候代碼不出現異常,但執行邏輯也會出現問題。關於這個問題,阿里巴巴 Java 開發手冊裏也有所說起。這裏引用一下:
【強制】不要在 foreach 循環裏進行元素的 remove/add 操做。remove 元素請使用 Iterator 方式,若是併發操做,須要對 Iterator 對象加鎖。
相關代碼(稍做修改)以下:
List<String> a = new ArrayList<String>(); a.add("1"); a.add("2"); for (String temp : a) { System.out.println(temp); if("1".equals(temp)){ a.remove(temp); } } }
相信有些朋友應該看過這個,而且也執行過上面的程序。上面的程序執行起來不會雖不會出現異常,但代碼執行邏輯上卻有問題,只不過這個問題隱藏的比較深。咱們把 temp 變量打印出來,會發現只打印了數字1
,2
沒打印出來。初看這個執行結果確實很讓人詫異,不明緣由。若是死摳上面的代碼,咱們很難找出緣由,此時須要稍微轉換一下思路。咱們都知道 Java 中的 foreach 是個語法糖,編譯成字節碼後會被轉成用迭代器遍歷的方式。因此咱們能夠把上面的代碼轉換一下,等價於下面形式:
List<String> a = new ArrayList<>(); a.add("1"); a.add("2"); Iterator<String> it = a.iterator(); while (it.hasNext()) { String temp = it.next(); System.out.println("temp: " + temp); if("1".equals(temp)){ a.remove(temp); } }
這個時候,咱們再去分析一下 ArrayList 的迭代器源碼就能找出緣由。
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such 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]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } // 省略不相關的代碼 }
咱們一步一步執行一下上面的代碼,第一次進入 while 循環時,一切正常,元素 1 也被刪除了。但刪除元素 1 後,就沒法再進入 while 循環,此時 it.hasNext() 爲 false。緣由是刪除元素 1 後,元素計數器 size = 1,而迭代器中的 cursor 也等於 1,從而致使 it.hasNext() 返回false。歸根結底,上面的代碼段沒拋異常的緣由是,循環提早結束,致使 next 方法沒有機會拋異常。不信的話,你們能夠把代碼稍微修改一下,便可發現問題:
List<String> a = new ArrayList<>(); a.add("1"); a.add("2"); a.add("3"); Iterator<String> it = a.iterator(); while (it.hasNext()) { String temp = it.next(); System.out.println("temp: " + temp); if("1".equals(temp)){ a.remove(temp); } }
以上是關於遍歷時刪除的分析,在平常開發中,咱們要避免上面的作法。正確的作法使用迭代器提供的刪除方法,而不是直接刪除。
4.總結
ArrayList 就是一個比較基礎的集合類,用的不少。它的結構簡單(本質上就是一個變長的數組),實現上也不復雜,感謝閱讀。