ArrayList 做爲 Java 集合框架中最經常使用的類,在通常狀況下,用它存儲集合數據最適合不過。知其然知其因此然,爲了能更好地認識和使用 ArrayList,本文將從下面幾方面深刻理解 ArrayList:html
在 Java 語言中,因爲普通數組受到長度限制,初始化時就須要限定數組長度,沒法根據元素個數動態擴容,而且 Java 數組供開發者調用方法有限,只有取元素,獲取數組長度和添加元素一些簡單操做。後臺在 Java 1.2 引入了強大豐富的 Collection 框架,其中用 ArrayList 來做爲可動態擴容數組的列表實現來代替 Array 在平常開發的使用,ArrayList 實現全部列表的操做方法,方便開發者操做列表集合。這裏咱們先列舉下 ArrayList 的主要特色,在後文進行一一闡述:java
有序存儲元素api
容許元素重複,容許存儲 null
值數組
支持動態擴容安全
非線程安全微信
爲了更好地認識 ArrayList,咱們首先來看下從 ArrayList 的UML類圖:多線程
從上圖能夠看出 ArrayList 繼承了 AbstractList, 直接實現了 Cloneable, Serializable,RandomAccess 類型標誌接口。併發
clone
方法,實現 ArrayList 的拷貝。serialVersionUID
屬性值。for
循環方式,而且執行效率上給迭代器方式更高。進入 ArrayList 源代碼,從類的結構裏很快就能看到 ArrayList 的兩個重要成員變量:elementData
和 size
。oracle
elementData
是一個 Object 數組,存放的元素,正是外部須要存放到 ArrayList 的元素,即 ArrayList 對象維護着這個對象數組 Object[],對外提供的增刪改查以及遍歷都是與這個數組有關,也所以添加到 ArrayList 的元素都是有序地存儲在數組對象 elementData
中。size
字段表示着當前添加到 ArrayList 的元素個數,須要注意的是它一定小於等於數組對象 elementData
的長度。一旦當 size
與 elementData
長度相同,而且還在往列表裏添加元素時,ArrayList 就會執行擴容操做,用一個更長的數組對象存儲先前的元素。因爲底層維護的是一個對象數組,因此向 ArrayList 集合添加的元素天然是能夠重複的,容許爲 null
的,而且它們的索引位置各不同。app
瞭解完 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
裏各位置存放的都是同樣元素的引用,一旦哪一個列表修改了數組中的某個元素,另外一個列表也將受到影響。
從源碼角度分析完 ArrayList 的特性以後,咱們再來看下 JDK 1.8 以後在 ArrayList 類上有什麼新的變化。
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]
複製代碼
這個方法也是來自於 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
複製代碼
ArrayList 提供兩個方法用於列表向數組的轉換
public Object[] toArray();
public <T> T[] toArray(T[] a);
複製代碼
從上述描述能夠看出使用第二個方法更加合適,能保留原先類型:
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 前半段時,就從頭開始遍歷,立刻找到後將把元素在相應的位置進行插入或者刪除操做。
到這裏咱們學習總結 ArrayList 的實現和常見使用,做爲基礎容器集合,越是多些瞭解,對咱們平常使用越順手。因爲上文提到了另外一個列表集合 LinkedList,它與 ArrayList 實現方式不一樣,使用場景也不一樣,將做爲下一篇文章分析的集合登場,感興趣的小夥伴歡迎關注個人微信公衆號,期待更新。