ArrayList是java容器中很重要很基礎的一部分,在面試中,容器相關的底層問題簡直不要太多,那麼對於其底層的東西,還是需要結合源碼(此處版本:jdk8)進行分析。
一 :數據結構
ArrayList底層數據結構核心其實是一個Object數組,對於ArrayList的操作都是基於這個Object進行操作而實現的。
二:內存模型/內存分配
這裏我貼上一張我自己畫的圖,因爲我在網上看到一些關於ArrayList的內存模型的說法其實是有問題的,比如說有人認爲無參初始化是構造一個默認容量爲10的數組(錯的);還有就是認爲ArrayList的地址其實就是底層的Object數組的地址(錯的,這個是我看到的網上一張很常見的圖的說法,裏面講到你new出來的ArrayList的地址就是Object[0]的地址),這裏可能你看不懂,後面我會解釋清楚。
三 : 繼承關係
從源碼上看,它繼承了AbstractList抽象父類,實現了List(規定了List的操作規範)、RandomAccess(可隨機訪問)、Cloneable(可拷貝)、Serializable(可序列化)幾個接口;這可以從源碼中看的到:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
這裏主要分析兩個地方:RandomAccess和Serializable
RandomAccess:(可隨機訪問)
查看RandomAccess源碼,會發現其並沒有任何內容,是一個標記接口;它只是用來標記說實現這個接口的List集合就具備快速隨機訪問的能力,也就是能快速地訪問List集合中的隨機任何一個元素。
同時,如果你認真查看RandomAccess源碼,你會看到註釋中還說到一個東西,就是:如果某個List實現了這個接口,那麼使用for循環方式來獲取數據的速度會快於使用迭代器的方式。
總結一下就是:
當一個List實現了RandomAccess接口,就意味着擁有快速訪問功能,其遍歷方法採用for循環最快速。而沒有快速訪問功能的List,遍歷的時候採用Iterator迭代器最快速。
這裏多說一個,LinkedList是基於鏈表實現的,其不具備快速隨機訪問能力,其遍歷需要遍歷實現,時間複雜度爲O(n);
附上一個小測試類吧:
@Test
public void RandomAccessTest(){
List<Integer> arrayList = new ArrayList<Integer>();
for (int i = 1; i <= 10000000; i++) {
arrayList.add(i);
}
long startTime = System.currentTimeMillis();
// for循環
System.out.println("此處使用for循環");
for (int i = 0;i< arrayList.size();i++) {
Object num = arrayList.get(i);
}
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime);
// 迭代器
System.out.println("採用迭代器遍歷");
startTime = System.currentTimeMillis();
Iterator it = arrayList.iterator();
while(it.hasNext()){
Object num = (it.next());
}
endTime = System.currentTimeMillis();
System.out.println(endTime-startTime);
}
輸出:
此處使用for循環
2
採用迭代器遍歷
4
Serializable:
這一部分需要放到最後再講,請先跳過
四:ArrayList的核心成員變量
直接附上源碼:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* The maximum size of array to allocate.
* 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;
DEFAULT_CAPACITY:這個是一個默認初始容量,爲10
EMPTY_ELEMENTDATA:一個共享空數組,具體作用後面講,注意它是static final
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:也是一個共享空數組,也是static final,但是它和EMPTY_ELEMENTDATA的區別在於DEFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素(add操作)時,知道要擴容多少(擴容,意思是擴大容量,具體後面講)
elementData:ArrayList的數組緩衝區,也就是數據結構那裏講到的ArrayList底層的那個Object數組;ArrayList的容量就是elementData的長度;這裏的註釋還講到,當添加第一個元素時,任何帶有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空ArrayList都將擴展爲DEFAULT_CAPACITY,也就是容量擴容爲10。
MAX_ARRAY_SIZE:elementData的最大值。這個的話很多人會問ArrayList的最大容量是多少,怎麼試,這個就是答案,但是實際上Integer的最大容量爲 0x7fffffff,接近2個g,你本地的話是到這個程度早崩了,而服務器的話雖然可以,但是沒必要做這樣的騷操作吧?
可能會有人對於EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA這兩個數組的存在有疑問,爲什麼要這樣規範呢?別急,後面有答案;
size:這個是ArrayList真正包含的元素的個數,它和容量是兩個概念;
五:構造函數
無參構造:
源碼如下:
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
由上面我們已經知道了,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空數組,雖然這裏的註釋寫的是構造一個初始容量爲10的空列表,但是,實際上並不是在進行無參構造的時候就去創建一個容量爲10的空數組了,實際上這個時候它還是空數組,只是之後進行第一次添加元素的時候,會和上面的關於DEFAULTCAPACITY_EMPTY_ELEMENTDATA的註釋的內容一樣,到那個時候才變成一個容量爲10 的列表,這也是我爲什麼上面在內存模型那裏說到的,認爲無參構造時就構造了默認容量爲10的空數組這樣的說法是錯的的原因。這裏附上我寫的驗證驗證ArrayList無參構造時數組的容量的鏈接,這裏會詳細講:驗證ArrayList無參構造時數組的容量
這裏可以加以思考的另一個東西,就是我在內存模型中講到的另一個東西:new出來的ArrayList的地址就是elementData的地址,思考一下,如果是這樣的話,由於上面驗證了無參構造新new出來的arrayList裏的elementData都是指向同一個位置,如果真如他們所講的那樣,那麼無參構造新new出來的ArrayList應該都是指向同一個位置,這樣就可以很清楚知道這種思路是錯的了吧。實際上,ArrayList的內存模型就像一個二維數組,並不複雜,可以參考上面圖片進行理解。
給定初始容量構造:
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
源碼具體邏輯如下:
當 傳入的初始容量initialCapacity > 0爲真時,創建一個大小爲initialCapacity的數組,並將引用賦給elementData;
當 傳入的初始容量initialCapacity = 0爲真時,將空數組EMPTY_ELEMENTDATA賦給elementData;
當 傳入的初始容量initialCapacity < 0爲真時,直接拋出IllegalArgumentException異常。
集合類中構造:
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
這裏源碼邏輯如下:
將傳入集合轉化爲數組,並賦值給elementData;判斷參數集合是否爲空,如果爲空,則將空數組EMPTY_ELEMENTDATA賦給elementData;
如果傳入參數集合不爲空,則判斷傳入參數轉換後的數組是否爲Object數組,如果不是,則利用淺拷貝將其轉化爲Object類型的數組。
這裏可能會有人有疑問,爲什麼一定要轉化爲Object數組,還有那句 // c.toArray might (incorrectly) not return Object[] (see
6260652)註釋是什麼意思,這裏不加以說明,具體原因看這篇:關於 c.toArray might (incorrectly) not return Object[]以及爲什麼一定要返回Object數組的原因
這裏總結幾個很重要的點:
Collection接口的toArray方法依賴於實現類中的具體實現,也就是說集合轉ArrayList之後元素的順序是由具體集合的toArray方法所決定的;
toArray可能會不正確地返回數組對象Object[].class
看完構造方法,可能對於之前在第四點哪裏提到的兩個空數組的區別你已經有了答案了,這裏也附上我寫的關於我對兩個空數組的一點理解吧(個人覺得對比jdk7會有更好的理解)。點擊:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA兩個空數組的區別
六:add操作以及ArrayList的擴容機
直接貼上一張我做的圖片:
從上面可以看出,擴容操作需要調用Arrays.copyOf()把原始副本整個複製到新副本中,,然後丟棄舊數組,這個操作代價很高,因此最好在創建ArrayList對象時就指定大概的容量大小,減少擴容操作的次數,這樣會減少數組創建和Copy的操作,還會減少內存使用。
七:indexOf(Object o)方法
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;
}
從源碼上看,這個方法的作用是從數組頭開始尋找和傳入的元素相等的元素,如果找到了,則返回其在數組中的位置(下標);若沒找到,則返回-1;這裏要注意的一點是,需要把情況分爲是null和不是null;因爲如果是null情況下還使用equals()方法會出現空指針異常。
八:get(int index)方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
get方法也是我們常用到的一個方法,作用是返回指定下標位置的元素的值;從源碼上看,它會先調用rangeCheck方法進行範圍檢查,只要你指定的下標不大於size的值,則返回elementData(index);在這裏,也會發生向下轉型
九:set(int index,E element)方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
set方法是我們修改指定下標位置的元素的值的方法,從源碼上看也會先進行範圍的檢查;然後把我們給定的值放到指定的位置,最後把舊的值返回;
十:remove(int index)方法
第一種:
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
remove(int index)方法的作用是刪除指定下標的元素,從效果上來看,是將指定下標後面一位到數組末尾的全部元素向前移動一個單位,所以,很多人會把這個現象當成是是過程,說刪除指定位置的元素就是把指定位置的元素後面的全部元素向前移動一個單位,實際上不是的;看源碼我們就知道;其實是調用 System.arraycopy() 將 index+1 後面的元素都複製到 index 位置上,然後把數組最後一個元素設置爲null;該操作的時間複雜度爲 O(N),可以看出 ArrayList 刪除元素的代價是非常高的。
第二種:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(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++;
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
}
仔細一看,是不是很熟悉;其實它的實現就是遍歷數組,如果發現存在傳入對象;那麼就調用fastRemove(int index)方法;而fastRemove基本和remove(int index)方法沒差別,只是少了一個範圍判斷而已,而爲什麼可以不用範圍判斷,是因爲既然你在數組內找到了這個對象,那麼肯定沒超過範圍,所以直接fastRemove方法來做數組刪除;真正的刪除元素的地方,和第一種其實是一樣的實現過程。
其實,還有一個方法:
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
執行過程是將elementData從toIndex位置開始的元素向前移動到fromIndex,然後將toIndex位置之後的元素全部置空順便修改size。
十一:補充
序列化:
在最上面的地方,我就寫到了ArrayList實現了序列化接口,並且說後面再解釋,沒錯,到這裏才解釋;其實也沒什麼,只有一兩個地方要注意下理解下的而已;比如在elementData定義的地方,可以看到使用了關鍵字 transient 修飾該關鍵字聲明數組默認不會被序列化;爲什麼這樣做呢?原因在於ArrayList 是基於數組實現,並且具有動態擴容特性,保存元素的數組不一定都會被使用,那麼就沒必要全部進行序列化。
其實;ArrayList 內部還實現了 writeObject() 和 readObject() 方法,來控制只序列化數組中有元素填充那部分內容。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
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
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
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();
}
}
}
這裏有個東西:
writeObject() 將對象轉換爲字節流並輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實現序列化。反序列化也是類似的道理。
這裏有一個點:在writeObject() 最後面有這樣的代碼:
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
這個modCount 看到這裏應該比較眼熟了,上面很多方法內部都存在着對modCount進行操作;實際上,modCount是用來記錄 ArrayList 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的所有操作,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。
而在進行序列化或者迭代等操作時,需要比較操作前後 modCount 是否改變,如果改變了需要拋出 ConcurrentModificationException。
關於淺克隆
可以看到的一點是,ArrayList的實現中大量地調用了Arrays.copyof()和System.arraycopy()方法。查源碼:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
可以看到的是,其實真正實現的地方是 System.arraycopy()這個方法,而查看源碼可以知道,這個方法被標記爲native方法;意味着這是在底層調用了C/C++庫的一個方法;實際上這個方法最終調用了C語言庫中的的memmove()函數,因此它可以保證同一個數組內元素的正確複製和移動,比一般的複製方法的實現效率要高很多,很適合用來批量處理數組。Java強烈推薦在複製大量數組元素時用該方法,以取得更高的效率。
十二:總結
ArrayList組存/取效率高:
ArrayList底層以數組實現,它實現了RandomAccess接口,且由於數組是連續存放元素的,找到第一個元素的首地址,再加上每個元素的佔據的字節大小就能定位到對應的元素。因此查找也就是get的時候非常快,時間複雜度是O(1),同樣的對於取特定位置的set操作也是一樣的。
ArrayList的添加元素操作:
當進行add操作的時候,最理想的情況就是不指定位置直接添加元素時(add(E element)),元素會默認會添加在最後,理想狀態下不會觸發底層數組的複製,也不用考慮底層數組的自動擴容,這種情況下時間複雜度爲O(1) ;但是假如是在指定位置添加元素(add(int index, E element)),需要複製底層數組,根據最壞打算,時間複雜度是O(n)。
ArrayList的插入刪除元素操作:
ArrayList的插入和刪除元素效率不高,每次插入或刪除元素,就要大量地移動元素,這兩種操作時間複雜度爲O(n)。當ArrayList裏有大量數據時,這時候去頻繁插入/刪除元素會觸發底層數組頻繁拷貝,效率低還會造成內存空間的浪費
結束語:
關於容器類的學習其實很讓我興奮,但是關於它的總結卻讓我非常頭疼,因爲我感覺要寫的能寫的太多了,同時還要整理整體的連接關係;但是,不管過程和結構都是很有意思的,特別是在畫上面兩張圖的時候;學習道路道阻且長,唯有不斷堅持,纔有資格說無愧於心,內心坦蕩。