簡介
- ArrayList是基於數組實現的,是一個動態數組,其容量能自動增長,類似於C語言中的動態申請內存,動態增長內存。
- ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent並發包下的CopyOnWriteArrayList類。
- ArrayList實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸,實現了RandomAccess接口,支持快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable接口,能被克隆。
存儲結構
// 當前數據對象存放地方,當前對象不參與序列化
// 這個關鍵字最主要的作用就是當序列化時,被transient修飾的內容將不會被序列化
transient Object[] elementData;
- Object類型數組。
數據域
// 序列化ID
private static final long serialVersionUID = 8683452581122892189L;
// 默認初始容量
private static final int DEFAULT_CAPACITY = 10;
// 一個空數組,方便使用,主要用於帶參構造函數初始化和讀取序列化對象等。
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 和官方文檔寫的一樣,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別
* 僅僅是為了區別用戶帶參為0的構造和默認構造的惰性初始模式對象。
* 當用戶帶參為0的構造,第一次add時,數組容量grow到1。
* 當用戶使用默認構造時,第一次add時,容量直接grow到DEFAULT_CAPACITY(10)。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 當前數據對象存放地方,當前對象不參與序列化
// 這個關鍵字最主要的作用就是當序列化時,被transient修飾的內容將不會被序列化
transient Object[] elementData; // non-private to simplify nested class access
// 當前數組中元素的個數
private int size;
// 數組最大可分配容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 集合數組修改次數的標識(由AbstractList繼承下來)(fail-fast機制)
protected transient int modCount = 0;
- ArrayList的無參構造函數。初始化的時候並沒有真正的創建10個空間,這是惰性初始模式對象。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別僅僅是為了區別用戶帶參為0的構造和默認構造的惰性初始模式對象。
- modCount用來記錄ArrayList結構發生變化的次數。用於Fail-Fast機制
構造函數
public ArrayList() {
// 只有這個地方會引用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 使用 EMPTY_ELEMENTDATA,在其他的多個地方可能會引用EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
// 把傳入集合傳化成[]數組並淺拷貝給elementData
elementData = c.toArray();
// 轉化后的數組長度賦給當前ArrayList的size,並判斷是否為0
if ((size = elementData.length) != 0) {
//c.toArray可能不會返回 Object[],可以查看 java 官方編號為 6260652 的 bug
if (elementData.getClass() != Object[].class)
// 若 c.toArray() 返回的數組類型不是 Object[],則利用 Arrays.copyOf(); 來構造一個大小為 size 的 Object[] 數組
// 此時elementData是指向傳入集合的內存,還需要創建新的內存區域深拷貝給elementData
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 傳入數組size為零替換空數組
this.elementData = EMPTY_ELEMENTDATA;
}
}
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別僅僅是為了區別用戶帶參為0的構造和默認構造的惰性初始模式對象。
- 注意深拷貝和淺拷貝。
- 帶參為0的構造會惰性初始化,不為0的構造則不會惰性初始化。
add()源碼解析
public boolean add(E e) {
// 確保數組已使用長度(size)加1之后足夠存下 下一個數據
ensureCapacityInternal(size + 1); // Increments modCount!!
// 數組的下一個index存放傳入元素。
elementData[size++] = e;
// 始終返回true。
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 這里就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和
// EMPTY_ELEMENTDATA 最主要的區別。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 默認構造第一次add返回10。
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 帶參為0構造第一次add返回 1 (0 + 1)。
return 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;
// 新數組容量為1.5倍的舊數組容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 若 newCapacity 依舊小於 minCapacity
newCapacity = minCapacity;
// 判斷是需要的容量是否超過最大的數組容量。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 在Arrays.copyOf()中會將原數組整個賦值到擴容的數組中。
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 擴容操作需要調用Arrays.copyOf()把原數組整個復制到新數組中,這個操作代價很高,因此最好在創建ArrayList對象時就指定大概的容量大小,減少擴容操作的次數。
add(int index, E element)源碼分析
// 這是一個本地方法,由C語言實現。
public static native void arraycopy(Object src, // 源數組
int srcPos, // 源數組要復制的起始位置
Object dest, // 目標數組(將原數組復制到目標數組)
int destPos, // 目標數組起始位置(從目標數組的哪個下標開始復制操作)
int length // 復制源數組的長度
);
public void add(int index, E element) {
// 判斷索引是否越界
rangeCheckForAdd(index);
// 確保數組已使用長度(size)加1之后足夠存下 下一個數據
ensureCapacityInternal(size + 1); // Increments modCount!!
// 運行到這里代表數組容量滿足。
// 數組從傳入形參index處開始復制,復制size-index個元素(即包括index在內后面的元素全部復制),
// 從數組的index + 1處開始粘貼。
// 這時,index 和 index + 1處元素數值相同。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 把index處的元素替換成新的元素。
elementData[index] = element;
// 數組內元素長度加一。
size++;
}
- 需要調用System.arraycopy()將包括index在內后面的元素都復制到index + 1位置上,該操作的時間復雜度為O(N),可以看出ArrayList數組頭增加元素的代價是非常高的。
remove(int index)源碼分析
public E remove(int index) {
// 檢查index
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 和 add(int index, E element)原理想通。
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 引用計數為0,會自動進行垃圾回收。
elementData[--size] = null; // clear to let GC do its work
// 返回舊元素
return oldValue;
}
- 需要調用System.arraycopy()將包括index + 1在內后面的元素都復制到index位置上,該操作的時間復雜度為O(N),可以看出ArrayList數組頭增加元素的代價是非常高的。
Fail-Fast機制
fail-fast 機制,即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制。當在迭代集合的過程中該集合在結構上發生改變的時候,就有可能會發生fail-fast,即拋出ConcurrentModificationException異常。fail-fast機制並不保證在不同步的修改下一定會拋出異常,它只是盡最大努力去拋出,所以這種機制一般僅用於檢測bug。- 結構發生變化是指添加或者刪除至少一個元素的所有操作,或者是調整內部數組大小,僅僅只是設置元素的值不算結構發生變化。
- 在進行序列化或者迭代操作時,需要比較操作前后modCount是否改變,如果改變了需要跑出ConcurrentModificationException
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
// 期待的修改值等於當前修改次數(modCount)
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 檢查 expectedModCount是否等於modCount,不相同則拋出ConcurrentModificationException
checkForComodification();
/** 省略此處代碼 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此處代碼 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
一個單線程環境下的fail-fast的例子
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0 ; i < 10 ; i++ ) {
list.add(i + "");
}
Iterator<String> iterator = list.iterator();
int i = 0 ;
while(iterator.hasNext()) {
if (i == 3) {
list.remove(3);
}
System.out.println(iterator.next());
i ++;
}
}
序列化
ArrayList 實現了 java.io.Serializable 接口,但是自己定義了序列化和反序列化。因為ArrayList基於數組實現,並且具有動態擴容特性,因此保存元素的數組不一定都會被使用,那么就沒有必要全部進行序列化。因此 elementData 數組使用 transient 修飾,可以防止被自動序列化。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
// 將當前類的非靜態(non-static)和非瞬態(non-transient)字段寫入流
// 在這里也會將size字段寫入。
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
// 序列化數組包含元素數量,為了向后兼容
// 兩次將size寫入流
s.writeInt(size);
// Write out all elements in the proper order.
// 按照順序寫入,只寫入到數組包含元素的結尾,並不會把數組的所有容量區域全部寫入
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
// 判斷是否觸發Fast-Fail
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 設置數組引用空數組。
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
// 將流中的的非靜態(non-static)和非瞬態(non-transient)字段讀取到當前類
// 包含 size
s.defaultReadObject();
// Read in capacity
// 讀入元素個數,沒什么用,只是因為寫出的時候寫了size屬性,讀的時候也要按順序來讀
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
// 根據size計算容量。
int capacity = calculateCapacity(elementData, size);
// SharedSecrets 一個“共享機密”存儲庫,它是一種機制,
// 用於調用另一個包中的實現專用方法,而不使用反射。TODO
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
// 檢查是否需要擴容
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
// 依次讀取元素到數組中
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList中為什么size要序列化兩次?
在代碼中s.defaultWriteObject();中size應該也被序列化了,為什么下邊還要再單獨序列化一次呢?
這樣寫是出於兼容性考慮。
舊版本的JDK中,ArrayList的實現有所不同,會對length字段進行序列化。
而新版的JDK中,對優化了ArrayList的實現,不再序列化length字段。
這個時候,如果去掉s.writeInt(size),那么新版本JDK序列化的對象,在舊版本中就無法正確讀取,
因為缺少了length字段。
因此這種寫法看起來多此一舉,實際上卻保證了兼容性。
小結
- ArrayList基於數組方式實現,無容量的限制(會擴容)
- 添加元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量可以使用trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所占的內存空間。
- 線程不安全
- add(int index, E element):添加元素到數組中指定位置的時候,需要將該位置及其后邊所有的元素都整塊向后復制一位
- get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))
- remove(Object o)需要遍歷數組
- remove(int index)不需要遍歷數組,只需判斷index是否符合條件即可,效率比remove(Object o)高
- contains(E)需要遍歷數組