“心繫天下”三星 W22 5G 手機 10 月 13 日釋出:預計為 Galaxy Z Fold3 升級版
概要
概括的說,ArrayList
是一個動態陣列,他是執行緒不安全的,允許元素為null。
其底層資料結構依然是陣列,它實現了List<E>
,RandomAccess
, Cloneable
, Serializable
介面,其中RandomAccess
代表了其擁有快速隨機訪問的能力,ArrayList
可以以O(1)的時間複雜度去根據下表訪問元素。
RandomAccess
: 標記介面, 標記實現該介面的集合使用索引遍歷比迭代器更快.
Serializable
: 標記介面, 標記實現該介面的類可以序列化。
Cloneable
: 標記介面, 標記實現該介面的類可以呼叫 clone 方法, 否則會丟擲CloneNotSupportedException
(克隆不被支援)異常.
因其底層資料結構是陣列,所以可想而知,它是佔據一塊連續的記憶體空間(容量就是陣列的length
),所以它也有陣列的缺點,空間效率不高。
由於陣列的記憶體連續,可以根據下標以O1的時間讀寫(改查)元素,因此時間效率很高。
當集合中的元素超出這個容量,便會進行擴容操作。擴容操作也是ArrayList
的一個性能消耗比較大的地方,所以若我們可以提前預知資料的規模,應該通過public ArrayList(int initialCapacity) {}
構造方法,指定集合的大小,去構建ArrayList
例項,以減少擴容次數,提高效率。
或者在需要擴容的時候,手動呼叫public void ensureCapacity(int minCapacity) {}
不過該方法是
ArrayList
的API,不是List
接口裡的,所以使用時需要強轉:((ArrayList)list).ensureCapacity(30)
;
當每次修改結構時,增加導致擴容,或者刪,都會修改modCount
。
成員變數
// 預設初始容量為10 private static final int DEFAULT_CAPACITY = 10; // 有參建構函式的陣列容量為0時,賦值給elementData的空陣列 private static final Object[] EMPTY_ELEMENTDATA = {}; // 預設無參建構函式裡的空陣列 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 儲存集合元素的底層實現:真正存放元素的陣列 transient Object[] elementData; // 當前陣列元素的數量 private int size; // 陣列最大容量 private static final int MAX_ARRAY_SIZE = 2147483639;
建構函式
// 預設建構函式
public ArrayList() {
//預設構造方法只是簡單的將空陣列賦值給了elementData
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//帶初始容量的構造方法
public ArrayList(int initialCapacity) {
//如果初始容量大於0,則新建一個長度為initialCapacity的Object陣列.
//注意這裡並沒有修改size(對比第三個建構函式)
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果容量為0,直接將EMPTY_ELEMENTDATA賦值給elementData
this.elementData = EMPTY_ELEMENTDATA;
} else {
//容量小於0,直接丟擲異常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//利用別的集合類來構建ArrayList的建構函式
public ArrayList(Collection<? extends E> c) {
//直接利用Collection.toArray()方法得到一個物件陣列,並賦值給elementData
elementData = c.toArray();
//因為size代表的是集合元素數量,所以通過別的集合來構造ArrayList時,要給size賦值
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
//這裡是當c.toArray出錯,沒有返回Object[]時,利用Arrays.copyOf 來複制集合c中的元素到elementData陣列中
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果集合c元素數量為0,則將空陣列EMPTY_ELEMENTDATA賦值給elementData
this.elementData = EMPTY_ELEMENTDATA;
}
}
常用API
增加
每次add之前,都會判斷add後的容量,是否需要擴容。
- 新增單個元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;//在陣列末尾追加一個元素,並修改size
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//利用 == 可以判斷陣列是否是用預設建構函式初始化的,如果是的話,minCapacity改為10和minCapacity中較大的那個
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//如果確定要擴容,會修改modCount
// 如果容量比當前陣列的容量大,那麼進行擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//需要擴容的話,預設擴容一半
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//預設擴容一半
if (newCapacity - minCapacity < 0)//如果還不夠 ,那麼就用 能容納的最小的數量。(add後的容量)
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) // 整數溢位
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 在index的位置新增一個元素,index開始的陣列,往後移動一位
public void add(int index, E element) {
rangeCheckForAdd(index);//越界判斷 如果越界拋異常
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //將index開始的資料 向後移動一位
elementData[index] = element;
size++;
}
- 新增多個元素
// 將指定集合中的所有元素按指定結合的迭代器返回的順序追加到此列表的末尾
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 從指定位置開始, 將指定集合中的元素插入此列表. 該位置及其之後的元素後移.
public boolean addAll(int index, Collection<? extends E> c) {
// 判斷索引是否超出界限, 超出丟擲 IndexOutOfBoundsException 異常.
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 確認是否需要擴容
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved); // 移動(複製陣列)
System.arraycopy(a, 0, elementData, index, numNew); // 複製陣列完成批量賦值
size += numNew;
return numNew != 0;
}
總結:
add、addAll
- 先判斷是否越界,是否需要擴容
- 如果擴容,就複製陣列
- 然後設定對應下標元素值
注意:
- 如果需要擴容的話,預設擴容一半。如果擴容一半不夠,就用目標的size作為擴容後的容量。
- 在擴容成功後,會修改modeCount。
刪除
public E remove(int index) {
rangeCheck(index); //判斷是否越界
modCount++; //修改modeCount 因為結構改變了
E oldValue = elementData(index); //讀出要刪除的值
int numMoved = size - index - 1;
// 如果不是最後一個元素
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//用複製 覆蓋陣列資料
// 置空原尾部資料 不再強引用, 可以GC掉
// 如果沒有elementData[--size]==null, 可能會導致記憶體洩漏, 若沒有這一步操作, 該記憶體一直指向之前的元素, GC 不會認為它是垃圾, 故無法回收記憶體造成記憶體洩漏.
elementData[--size] = null;
return oldValue;
}
//根據下標從陣列取值 並強轉
E elementData(int index) {
return (E) elementData[index];
}
//刪除該元素在陣列中第一次出現的位置上的資料。 如果有該元素返回true,如果false。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);//根據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++;//修改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 //置空 不再強引用
}
//批量刪除
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);//判空
return batchRemove(c, false);
}
//批量移動
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;//w 代表批量刪除後 陣列還剩多少元素
boolean modified = false;
try {
//高效的儲存兩個集合公有元素的演算法
for (; r < size; r++)
if (c.contains(elementData[r]) == complement) // 如果 c裡不包含當前下標元素,
elementData[w++] = elementData[r];//則保留
} finally {
// 出現異常會導致 r !=size , 則將出現異常處後面的資料全部複製覆蓋到數組裡。
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r; //修改 w 數量
}
if (w != size) {//置空陣列後面的元素
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;//修改modCount
size = w;// 修改size
modified = true;
}
}
return modified;
}
從這裡我們也可以看出,當用來作為刪除元素的集合裡的元素多於被刪除集合時,也沒事,只會刪除它們共同擁有的元素。
總結:
刪除操作一定會修改modCount,且可能涉及到陣列的複製,相對低效。
修改
不會修改modCount,相對於增刪,修改是高效的操作。
// 用指定元素替換列表中指定位置的元素.
public E set(int index, E element) {
rangeCheck(index);//越界檢查
E oldValue = elementData(index); //取出舊元素
elementData[index] = element;//用新元素覆蓋舊元素
return oldValue;//返回舊元素
}
查詢
不會修改modCount,相對於增刪,查詢是高效的操作。
// 告訴編譯器忽略 unchecked 警告資訊
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
// 獲取列表指定位置的元素.
public E get(int index) {
// 越界檢查
rangeCheck(index);
return elementData(index);
}
// 返回指定元素第一次出現的索引,如果列表中沒有此元素, 返回 -1
public int indexOf(Object o) {
if (o == null) {
// null 也當成一個元素
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// 返回指定元素最後一次出現的索引,如果列表中沒有此元素, 返回 -1
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
清空 clear
會修改modeCount
public void clear() { modCount++;//修改modCount for (int i = 0; i < size; i++) //將所有元素置null,方便GC去清理 elementData[i] = null; size = 0; //修改size }
包含 contain
// indexOf() 普通的for迴圈尋找值,只不過會根據目標物件是否為null分別迴圈查詢。public boolean contains(Object o) { return indexOf(o) >= 0;}// 返回指定元素第一次出現的索引,如果列表中沒有此元素, 返回 -1public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1;}
判空 isEmpty
public boolean isEmpty() { return size == 0;}
縮容 trimToSize
ArrayList雖然沒有自動縮容,但是提供了trimToSize方法讓使用者可以進行手動擴容。
public void trimToSize() { // 如果elementData的長度大於size,則說明有剩餘空間,可以進行縮容 ++this.modCount; if (this.size < this.elementData.length) { this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size); }}
轉為陣列 toArray
// 按正確順序包含此列表中所有元素的陣列,返回的陣列為 elementData 的複製// 修改返回的陣列不會對 elementData 造成影響.public Object[] toArray() { // 直接複製 elementData, 然後返回 return Arrays.copyOf(elementData, size);}// 返回執行時指定陣列型別的陣列.// 如果指定陣列的容量小於 size, 新建一個容量為size的陣列返回.// 否則將資料複製到指定陣列返回.@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) { if (a.length < size) // 利用反射, 返回一個大小為size , 型別和 a 相同的新陣列. return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 將資料複製到 a 中 System.arraycopy(elementData, 0, a, 0, size); // 將 a[size] 置為 null(如果有的話), 對以後確定列表的長度很有用, 但只在呼叫方知道列表中不包含任何 null 元素時才有用. if (a.length > size) a[size] = null; return a;}
System.arrayCopy()引數意義
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)Object src : 原陣列int srcPos : 從原資料的起始位置開始Object dest : 目標陣列int destPos : 目標陣列的開始起始位置int length : 要copy的陣列的長度
問題
elementData 為什麼用 transient 修飾?
Java的ArrayList
中,定義了一個數組elementData
用來裝載物件的,具體定義如下:
transient Object[] elementData;
transient用來表示一個域不是該物件序行化的一部分,當一個物件被序行化的時候,transient
修飾的變數的值是不包括在序行化的表示中的。但是ArrayList
又是可序行化的類,elementData
是ArrayList
具體存放元素的成員,用transient
來修飾elementData
,豈不是反序列化後的ArrayList
丟失了原先的元素?
其實玄機在於ArrayList中的兩個方法:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{}private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { }
ArrayList
在序列化的時候會呼叫writeObject,直接將size和element寫入ObjectOutputStream
;
反序列化時呼叫readObject,從ObjectInputStream
獲取size和element,再恢復到elementData
。
那麼為什麼不直接用elementData來序列化?
而是採用上述的方式來實現序列化呢?原因在於elementData
是一個快取陣列,它通常會預留一些容量,等容量不夠的時候再擴容容量,那麼有些空間可能就沒有實際儲存元素,我們不想在序列化的時候把這些空資料序列化,所以對elementData
新增transient
,使用自定義的方式進行序列化。
為什麼 hugeCapacity() 會返回 Integer.MAX_VALUE?
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 整數溢位 throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
/** * The maximum size of array to allocate (unless necessary). * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
是因為部分虛擬機器在陣列中儲存header words的頭部位元組需要佔用8個位元組,在這些虛擬機器中分配大於MAX_ARRAY_SIZE
的空間會導致OOM,而在其他虛擬機器中則可以正常擴容。
而grow
方法的目的是:提高容量以便至少滿足最少的minCapacity
容量。
因為陣列理論上的長度就是Integer.MAX_VALUE
,但並不是說一定因為個別的JVM就不讓擴容到整數最大值的長度。