前言java
ArrayList 做爲 Java 集合框架中最經常使用的類,在通常狀況下,用它存儲集合數據最適合不過。知其然知其因此然,爲了能更好地認識和使用 ArrayList,本文將從下面幾方面深刻理解 ArrayList:segmentfault
•爲何不用數組,用 ArrayList數組
•ArrayList 特性的源碼分析安全
•Java 8 後 的 ArrayList多線程
•正確的 ArrayList 使用姿式併發
在 Java 語言中,因爲普通數組受到長度限制,初始化時就須要限定數組長度,沒法根據元素個數動態擴容,而且 Java 數組供開發者調用方法有限,只有取元素,獲取數組長度和添加元素一些簡單操做。後臺在 Java 1.2 引入了強大豐富的 Collection 框架,其中用 ArrayList 來做爲可動態擴容數組的列表實現來代替 Array 在平常開發的使用,ArrayList 實現全部列表的操做方法,方便開發者操做列表集合。這裏咱們先列舉下 ArrayList 的主要特色,在後文進行一一闡述:app
•有序存儲元素框架
•容許元素重複,容許存儲 null 值dom
•支持動態擴容ide
•非線程安全
爲了更好地認識 ArrayList,咱們首先來看下從 ArrayList 的UML類圖:
從上圖能夠看出 ArrayList 繼承了 AbstractList, 直接實現了 Cloneable, Serializable,RandomAccess 類型標誌接口。
•AbstractList 做爲列表的抽象實現,將元素的增刪改查都交給了具體的子類去實現,在元素的迭代遍歷的操做上提供了默認實現。
•Cloneable 接口的實現,表示了 ArrayList 支持調用 Object 的 clone 方法,實現 ArrayList 的拷貝。
•Serializable 接口實現,說明了 ArrayList 還支持序列化和反序列操做,具備固定的 serialVersionUID 屬性值。
•RandomAccess 接口實現,表示 ArrayList 裏的元素能夠被高效效率的隨機訪問,如下標數字的方式獲取元素。實現 RandomAccess 接口的列表上在遍歷時可直接使用普通的for循環方式,而且執行效率上給迭代器方式更高。
進入 ArrayList 源代碼,從類的結構裏很快就能看到 ArrayList 的兩個重要成員變量:elementData 和 size。
•elementData 是一個 Object 數組,存放的元素,正是外部須要存放到 ArrayList 的元素,即 ArrayList 對象維護着這個對象數組 Object[],對外提供的增刪改查以及遍歷都是與這個數組有關,也所以添加到 ArrayList 的元素都是有序地存儲在數組對象 elementData 中。
•size 字段表示着當前添加到 ArrayList 的元素個數,須要注意的是它一定小於等於數組對象 elementData 的長度。一旦當 size 與 elementData 長度相同,而且還在往列表裏添加元素時,ArrayList 就會執行擴容操做,用一個更長的數組對象存儲先前的元素。
因爲底層維護的是一個對象數組,因此向 ArrayList 集合添加的元素天然是能夠重複的,容許爲 null 的,而且它們的索引位置各不同。
瞭解完 ArrayList 爲什麼有序存儲元素和元素能夠重複,咱們再來看下做爲動態數組列表,底層擴容是如何實現的。
首先,要肯定下擴容的時機會是在哪裏,就如上面描述 size 字段時提到的,當 size 與 elementData 長度相同,此刻再添加一個元素到集合就會出現容量不夠的狀況,須要進行擴容,也就是說 ArrayList 的擴容操做發生在添加方法中,而且知足必定條件時纔會發生。
如今咱們再來看下 ArrayList 類的代碼結構,能夠看到有四個添加元素的方法,分爲兩類:添加單個元素和添加另外一個集合內的全部元素。
先從簡單的方法下手分析,查看 add(E):boolean 方法實現:
public boolean add(E e) {
ensureCapacityInternal(size + 1); elementData[size++] = e; return true;
}
從上面能夠看出第三行代碼是簡單地添加單個元素,並讓 size 遞增長 1;那麼擴容實現就在 ensureCapacityInternal 方法中,這裏傳入參數爲 size+1,就是要在真正添加元素前判斷添加後的元素個數,也就是集合所須要的最小容量是否會超過原數組的長度。再看下這個 ensureCapacityInternal 方法實現
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData,minCapacity));
}
其內部仍有兩個方法調用,首先看下比較簡單的 calculateCapacity 方法:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity;
}
當 elementData 與 DEFAULTCAPACITY_EMPTY_ELEMENTDATA相等,也就是空數組時,返回一個可添加元素的默認最小容量值 DEFAULT_CAPACITY 對應的10 ,不然按照傳入的 size +1 爲最小容量值;執行完以後接着看 ensureExplicitCapacity 方法:
private void ensureExplicitCapacity(int minCapacity) {
modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity);
}
從代碼中能夠看到擴容實如今 grow 方法之中,而且只有當數組長度小於所須要的最小容量時執行:當數組存儲元素已滿,沒法再存儲將新加入的元素。
private void grow(int minCapacity) {
int oldCapacity = elementData.length; 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);
}
進一步跳轉到 grow 方法的實現,能夠看到第8行利用工具類方法 java.util.Arrays#copyOf(T[], int) ,對原有數組進行拷貝,將內部全部的元素存放到長度爲 newCapacity 的新數組中,並將對應新數組的引用賦值給 elementData。此刻 ArrayList 內部引用的對象就是更新長度了的新數組,實現效果就以下圖同樣:
如今咱們再來關注下表明數組新容量的 newCapacity 被調整爲多少。首先 newCapacity 經過 oldCapacity + (oldCapacity >> 1) 計算得到,使用位運算將原容量值 oldCapacity 經過右移一位,得到其一半的值(向下取整), 而後加上原來的容量值,那麼就是原容量值 oldCapacity的1.5倍。
右位運算符,會將左操做數進行右移,至關於除以2,而且向下取整,好比表達式 (7 >> 1) == 3 結果爲真。
當計算獲得的 newCapacity 仍然小於傳入最小容量值時,說明當前數組個數爲空,採用默認的 DEFAULT_CAPACITY做爲容量值分配數組。
額外須要注意的是還有最大數組個數的判斷,MAX_ARRAY_SIZE 在文件對應的代碼定義以下:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
ArrayList 存儲元素個數有最大限制,若是超過限制就會致使 JVM 拋出 OutOfMemoryError 異常。
到這裏 java.util.ArrayList#add(E) 方法的擴容邏輯就分析結束了。相似的,在其餘添加元素的方法裏實現內咱們均可以看到 ensureCapacityInternal 方法的調用,在真正操做底層數組前都會進行容量的確認,容量不夠則進行動態擴容。
transient Object[] elementData;
在 ArrayList 源碼看到的 elementData 帶有關鍵字 transient,而一般 transient 關鍵字修飾了字段則表示該字段不會被序列化,可是 ArrayList 實現了序列化接口,而且提供的序列化方法 writeObject 與反序列化方法 readObject 的實現, 這是如何作到的呢?
咱們首先來看下 ArrayList 進行序列化的代碼:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { int expectedModCount = modCount; s.defaultWriteObject(); s.writeInt(size); for (int i = 0; i < size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
第4行代碼首先將當前對象的非 static 修飾,非 transient 修飾的字段寫出到流中;第6行將寫出元素的個數做爲容量。
接下來就是經過循環將包含的全部元素寫出到流,在這一步能夠看出 ArrayList 在本身實現的序列化方法中沒有將無存儲數據的內存空間進行序列化,節省了空間和時間。
一樣地,在反序列化中根據讀進來的流數據中獲取 size 屬性,而後進行數組的擴容,最後將流數據中讀到的全部元素數據存放到持有的對象數組中。
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; s.defaultReadObject(); s.readInt(); // ignored if (size > 0) { int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; for (int i = 0; i < size; i++) { a[i] = s.readObject(); } } }
關於拷貝
針對列表元素的拷貝,ArrayList 提供自定義的 clone 實現以下:
public Object clone() {
try { 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); } }
從上述代碼能夠清楚看出執行的 copyOf 操做是一次淺拷貝操做,原 ArrayList 對象的元素不會被拷貝一份存到新的 ArrayList 對象而後返回,它們各自的字段 elementData 裏各位置存放的都是同樣元素的引用,一旦哪一個列表修改了數組中的某個元素,另外一個列表也將受到影響。
JDK 1.8 後的 ArrayList
從源碼角度分析完 ArrayList 的特性以後,咱們再來看下 JDK 1.8 以後在 ArrayList 類上有什麼新的變化。
新增 removeIf 方法
removeIf 是 Collection 接口新增的接口方法,ArrayList 因爲父類實現該接口,因此也有這個方法。removeIf 方法用於進行指定條件的從數組中刪除元素。
public boolean removeIf(Predicate<? super E> filter){...}
傳入一個表明條件的函數式接口參數 Predicate,也就是Lambda 表達式進行條件匹配,若是條件爲 true, 則將該元素從數組中刪除,例以下方代碼示例:
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); numbers.removeIf(i -> i % 2 == 0); System.out.println(numbers); // [1, 3, 5, 7, 9]
新增 spliterator 方法
這個方法也是來自於 Collection 接口,ArrayList 對此方法進行了重寫。該方法會返回 ListSpliterator 實例,該實例用於遍歷和分離容器所存儲的元素。
@Override public Spliterator<E> spliterator() { return new ArrayListSpliterator<>(this, 0, -1, 0); }
在 ArrayList 的實現中,該方法返回一個內部靜態類對象 ArrayListSpliterator,經過它能夠就能夠集合元素進行操做。
它的主要操做方法有下面三種:
•tryAdvance 迭代單個元素,相似於 iterator.next()
•forEachRemaining 迭代剩餘元素
•trySplit 將元素切分紅兩部分並行處理,但須要注意的 Spliterator 並非線程安全的。
雖然這個三個方法不經常使用,仍是有必要了解,能夠簡單看下方法的使用方式
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(1,2,3,4,5,6)); Spliterator<Integer> numbers = numbers.spliterator(); numbers.tryAdvance( e -> System.out.println( e ) ); // 1 numbers.forEachRemaining( e -> System.out.println( e ) ); // 2 3 4 5 6 Spliterator<Integer> numbers2 = numbers.trySplit(); numbers.forEachRemaining( e -> System.out.println( 3 ) ); //4 5 6 numbers2.forEachRemaining( e -> System.out.println( 3 ) ); //1 2 3
必會的使用姿式
接觸了 ArrayList 源碼和新API 以後,咱們最後學習如何在日常開發中高效地使用 ArrayList。
高效的初始化
ArrayList 實現了三個構造函數, 默認建立時會分配到空數組對象 EMPTY_ELEMENTDATA;第二個是傳入一個集合類型數據進行初始化;第三個容許傳入集合長度的初始化值,也就是數組長度。因爲每次數組長度不夠會致使擴容,從新申請更長的內存空間,並進行復制。而讓咱們初始化 ArrayList 指定數組初始大小,能夠減小數組的擴容次數,提供性能。
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; } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { this.elementData = EMPTY_ELEMENTDATA; } }
元素遍歷
JDK 1.8前,ArrayList 只支持3種遍歷方式:迭代器遍歷,普通 for 循環,for-each 加強,在 JDK1.8 引入了 Stream API 以後,同屬於 Collection 集合的 ArrayList,可使用 stream.foreach() 方法一個個地獲取元素:
ArrayList<String> names = new ArrayList<String>(Arrays.asList( "alex", "brian", "charles")); names.forEach(name -> System.out.println(name)); // alex brian charles
轉換 Array
ArrayList 提供兩個方法用於列表向數組的轉換
public Object[] toArray(); public <T> T[] toArray(T[] a);
1.第一個方法直接返回 Object 類型數組
2.在第二個方法中,返回數組的類型爲所傳入的指定數組的類型。而且若是列表的長度符合傳入的數組,將元素拷貝後數組後,則在其中返回數組。不然,將根據傳入數組的類型和列表的大小從新分配一個新數組,拷貝完成後再返回。
從上述描述能夠看出使用第二個方法更加合適,能保留原先類型:
ArrayList<String> list = new ArrayList<>(4); list.add("A"); list.add("B"); list.add("C"); list.add("D"); String[] array = list.toArray(new String[list.size()]); System.out.println(Arrays.toString(array)); // [A, B, C, D]
應對多線程
在這裏須要說明的是 ArrayList 自己是非線程安全的,若是須要使用線程安全的列表一般採用的方式是 java.util.Collections#synchronizedList(java.util.List<T>) 或者 使用 Vector 類代替。還有一種方式是使用併發容器類 CopyOnWriteArrayList 在多線程中使用,它底層經過建立原數組的副原本實現更新,添加等本來需同步的操做,不只線程安全,減小了對線程的同步操做。
應對頭部結點的增刪
ArrayList是數組實現的,使用的是連續的內存空間,當有在數組頭部將元素添加或者刪除的時候,須要對頭部之後的數據進行復制並從新排序,效率很低。針對有大量相似操做的場景,出於性能考慮,咱們應該使用 LinkedList 代替。因爲LinkedList 是基於鏈表實現,當須要操做的元素位置位於List 前半段時,就從頭開始遍歷,立刻找到後將把元素在相應的位置進行插入或者刪除操做。
QQ討論羣組:984370849 706564342 歡迎加入討論
想要深刻學習的同窗們能夠加入QQ羣討論,有全套資源分享,經驗探討,沒錯,咱們等着你,分享互相的故事!