一、ArrayList的數據結構
ArrayList的底層數據結構就是一個數組,數組元素的類型為Object類型,對ArrayList的所有操作底層都是基於數組的。
二、ArrayList的線程安全性
對ArrayList進行添加元素的操作的時候是分兩個步驟進行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步將size的值增加1。由於這個過程在多線程的環境下是不能保證具有原子性的,因此ArrayList在多線程的環境下是線程不安全的。
具體舉例說明:在單線程運行的情況下,如果Size = 0,添加一個元素后,此元素在位置 0,而且Size=1;而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也向此ArrayList 添加元素,因為此時 Size 仍然等於 0 (注意哦,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),所以線程B也將元素存放在位置0。然后線程A和線程B都繼續運行,都增 加 Size 的值。 那好,現在我們來看看 ArrayList 的情況,元素實際上只有一個,存放在位置 0,而Size卻等於 2。這就是“線程不安全”了。
如果非要在多線程的環境下使用ArrayList,就需要保證它的線程安全性,通常有兩種解決辦法:第一,使用synchronized關鍵字;第二,可以用Collections類中的靜態方法synchronizedList();對ArrayList進行調用即可。
三、ArrayList的繼承關系
ArrayList繼承AbstractList抽象父類,實現了List接口(規定了List的操作規范)、RandomAccess(可隨機訪問)、Cloneable(可拷貝)、Serializable(可序列化)。
四、ArrayList的主要成員變量
- private static final int DEFAULT_CAPACITY = 10;
當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組的容量大小,為10。
- private static final Object[] EMPTY_ELEMENTDATA = {};
當ArrayList的構造方法中顯示指出ArrayList的數組長度為0時,類內部將EMPTY_ELEMENTDATA 這個空對象數組賦給elemetData數組。
- private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組為DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
- transient Object[] elemetData;
ArrayList的底層數據結構,只是一個對象數組,用於存放實際元素,並且被標記為transient,也就意味着在序列化的時候此字段是不會被序列化的。
- private int size;
實際ArrayList中存放的元素的個數,默認時為0個元素。
- private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;
ArrayList中的對象數組的最大數組容量為Integer.MAX_VALUE – 8。
-
public class ArrayList<E> extends AbstractList<E>
-
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
{
-
// 版本號
-
private static final long serialVersionUID = 8683452581122892189L;
-
// 缺省容量
-
private static final int DEFAULT_CAPACITY = 10;
-
// 空對象數組
-
private static final Object[] EMPTY_ELEMENTDATA = {};
-
// 缺省空對象數組
-
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
-
// 元素數組
-
transient Object[] elementData;
-
// 實際元素大小,默認為0
-
private int size;
-
// 最大數組容量
-
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
-
}
五、ArrayList的構造方法
對於無參構造方法,將成員變量elementData的值設為DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
-
public ArrayList() {
-
// 無參構造函數,設置元素數組為空
-
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
-
}
參數為希望的ArrayList的數組的長度,initialCapacity。首先要判斷參數initialCapacity與0的大小關系:
如果initialCapacity大於0,則創建一個大小為initialCapacity的對象數組賦給elementData。
如果initialCapacity等於0,則將EMPTY_ELEMENTDATA賦給elementData。
如果initialCapacity小於0,拋出異常(非法的容量)。
-
public ArrayList(int initialCapacity) {
-
if (initialCapacity > 0) { // 初始容量大於0
-
this.elementData = new Object[initialCapacity]; // 初始化元素數組
-
} else if (initialCapacity == 0) { // 初始容量為0
-
this.elementData = EMPTY_ELEMENTDATA; // 為空對象數組
-
} else { // 初始容量小於0,拋出異常
-
throw new IllegalArgumentException("Illegal Capacity: "+
-
initialCapacity);
-
}
-
}
第一步,將參數中的集合轉化為數組賦給elementData;
第二步,參數集合是否是空。通過比較size與第一步中的數組長度的大小。
第三步,如果參數集合為空,則設置元素數組為空,即將EMPTY_ELEMENTDATA賦給elementData;
第四步,如果參數集合不為空,接下來判斷是否成功將參數集合轉化為Object類型的數組,如果轉化成Object類型的數組成功,則將數組進行復制,轉化為Object類型的數組。
-
public ArrayList(Collection<? extends E> c) { // 集合參數構造函數
-
elementData = c.toArray(); // 轉化為數組
-
if ((size = elementData.length) != 0) { // 參數為非空集合
-
if (elementData.getClass() != Object[].class) // 是否成功轉化為Object類型數組
-
elementData = Arrays.copyOf(elementData, size, Object[].class); // 不為Object數組的話就進行復制
-
} else { // 集合大小為空,則設置元素數組為空
-
this.elementData = EMPTY_ELEMENTDATA;
-
}
-
}
六、ArrayList的add()方法
在add()方法中主要完成了三件事:首先確保能夠將希望添加到集合中的元素能夠添加到集合中,即確保ArrayList的容量(判斷是否需要擴容);然后將元素添加到elementData數組的指定位置;最后將集合中實際的元素個數加1。
-
public boolean add(E e) { // 添加元素
-
ensureCapacityInternal(size + 1); // Increments modCount!!
-
elementData[size++] = e;
-
return true;
-
}
七、ArrayList的擴容機制
ArrayList的擴容主要發生在向ArrayList集合中添加元素的時候。由add()方法的分析可知添加前必須確保集合的容量能夠放下添加的元素。主要經歷了以下幾個階段:
第一,在add()方法中調用ensureCapacityInternal(size + 1)方法來確定集合確保添加元素成功的最小集合容量minCapacity的值。參數為size+1,代表的含義是如果集合添加元素成功后,集合中的實際元素個數。換句話說,集合為了確保添加元素成功,那么集合的最小容量minCapacity應該是size+1。在ensureCapacityInternal方法中,首先判斷elementData是否為默認的空數組,如果是,minCapacity為minCapacity與集合默認容量大小中的較大值。
第二,調用ensureExplicitCapacity(minCapacity)方法來確定集合為了確保添加元素成功是否需要對現有的元素數組進行擴容。首先將結構性修改計數器加一;然后判斷minCapacity與當前元素數組的長度的大小,如果minCapacity比當前元素數組的長度的大小大的時候需要擴容,進入第三階段。
第三,如果需要對現有的元素數組進行擴容,則調用grow(minCapacity)方法,參數minCapacity表示集合為了確保添加元素成功的最小容量。在擴容的時候,首先將原元素數組的長度增大1.5倍(oldCapacity + (oldCapacity >> 1)),然后對擴容后的容量與minCapacity進行比較:① 新容量小於minCapacity,則將新容量設為minCapacity;②新容量大於minCapacity,則指定新容量。最后將舊數組拷貝到擴容后的新數組中。
-
private void ensureCapacityInternal(int minCapacity) {
-
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判斷元素數組是否為空數組
-
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取較大值
-
}
-
-
ensureExplicitCapacity(minCapacity);
-
}
-
private void ensureExplicitCapacity(int minCapacity) {
-
// 結構性修改加1
-
modCount++;
-
if (minCapacity - elementData.length > 0)
-
grow(minCapacity);
-
}
-
private void grow(int minCapacity) {
-
int oldCapacity = elementData.length; // 舊容量
-
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量為舊容量的1.5倍
-
if (newCapacity - minCapacity < 0) // 新容量小於參數指定容量,修改新容量
-
newCapacity = minCapacity;
-
if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大於最大容量
-
newCapacity = hugeCapacity(minCapacity); // 指定新容量
-
// 拷貝擴容
-
elementData = Arrays.copyOf(elementData, newCapacity);
-
}
八、ArrayList的set(int index,E element)方法
set(int index, E element)方法的作用是指定下標索引處的元素的值。在ArrayList的源碼實現中,方法內首先判斷傳遞的元素數組下標參數是否合法,然后將原來的值取出,設置為新的值,將舊值作為返回值返回。
-
public E set(int index, E element) {
-
// 檢驗索引是否合法
-
rangeCheck(index);
-
// 舊值
-
E oldValue = elementData(index);
-
// 賦新值
-
elementData[index] = element;
-
// 返回舊值
-
return oldValue;
-
}
九、ArrayList的indexOf(Object o)方法
indexOf(Object o)方法的作用是從頭開始查找與指定元素相等的元素,如果找到,則返回找到的元素在元素數組中的下標,如果沒有找到返回-1。與該方法類似的是lastIndexOf(Object o)方法,該方法的作用是從尾部開始查找與指定元素相等的元素。
查看該方法的源碼可知,該方法從需要查找的元素是否為空的角度分為兩種情況分別討論。這也意味着該方法的參數可以是null元素,也意味着ArrayList集合中能夠保存null元素。方法實現的邏輯也比較簡單,直接循環遍歷元素數組,通過equals方法來判斷對象是否相同,相同就返回下標,找不到就返回-1。這也解釋了為什么要把情況分為需要查找的對象是否為空兩種情況討論,不然的話空對象調用equals方法則會產生空指針異常。
-
// 從首開始查找數組里面是否存在指定元素
-
public 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;
-
}
十、ArrayList的get(int index)方法
get(int index)方法是返回指定下標處的元素的值。get函數會檢查索引值是否合法(只檢查是否大於size,而沒有檢查是否小於0)。如果所引致合法,則調用elementData(int index)方法獲取值。在elementData(int index)方法中返回元素數組中指定下標的元素,並且對其進行了向下轉型。
-
public E get(int index) {
-
// 檢驗索引是否合法
-
rangeCheck(index);
-
-
return elementData(index);
-
}
-
E elementData(int index) {
-
return (E) elementData[index];
-
}
十一、ArrayList的remove(int index)方法
remove(int index)方法的作用是刪除指定下標的元素。在該方法的源碼中,將指定下標后面一位到數組末尾的全部元素向前移動一個單位,並且把數組最后一個元素設置為null,這樣方便之后將整個數組不再使用時,會被GC,可以作為小技巧。而需要移動的元素個數為:size-index-1。
-
public E remove(int index) {
-
// 檢查索引是否合法
-
rangeCheck(index);
-
-
modCount++;
-
E oldValue = elementData(index);
-
// 需要移動的元素的個數
-
int numMoved = size - index - 1;
-
if (numMoved > 0)
-
System.arraycopy(elementData, index+ 1, elementData, index,
-
numMoved);
-
// 賦值為空,有利於進行GC
-
elementData[--size] = null;
-
// 返回舊值
-
return oldValue;
-
}
十二、ArrayList的優缺點
- ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快。
- ArrayList在順序添加一個元素的時候非常方便,只是往數組里面添加了一個元素而已。
- 根據下標遍歷元素,效率高。
- 根據下標訪問元素,效率高。
- 可以自動擴容,默認為每次擴容為原來的1.5倍。
- 插入和刪除元素的效率不高。
- 根據元素下標查找元素需要遍歷整個元素數組,效率不高。
- 線程不安全。