又是熱愛學習的一天… 今天準備學習一下 ArrayList 的源碼,研究它都幹了什麼。
總結 (先看結論,再尋找如何得出的此結論)
-
ArrayList 是一種以數組實現的 List,與數組相比,它具有動態擴展的能力,因此也可稱之爲 動態數組。
-
ArrayList 實現了 List,提供了基礎的添加、刪除、遍歷等操作。
-
ArrayList 實現了 RandomAccess,提供了隨機訪問的能力。
-
ArrayList 實現了 Cloneable,可以被克隆。
-
ArrayList 實現了 Serializable,可以被序列化。
1. ArrayList 的成員變量
先看看 ArrayList 的成員變量,我把它的每一個成員變量都加了註釋用以解釋這個變量有何作用。
/**
* 默認初始容量,也就是說,使用 new ArrayList() 創建的 ArrayList ,它的初始容量爲 DEFAULT_CAPACITY;
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空數組,使用 new ArrayList(0) 創建 ArrayList 時,使用的數組
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空數組,使用 new ArrayList() 創建 ArrayList 時,使用的數組。
* 在添加第一個元素的時候,會將這個數組的容量初始化爲 DEFAULT_CAPACITY 大小
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存放真正的元素數據的數組
* 在添加第一個元素的時候,會使 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 數組容量初始化爲 DEFAULT_CAPACITY 大小,這和上面那句話對應起來了。
* 疑問?這裏加上 transient 關鍵字的意思應該是爲了不讓序列化這個數組裏面的內容,也就是我們存進 ArrayList的真實數據,可是經過測驗,卻可以序列化該數組裏面的數據。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的實際元素個數,而不是 ArrayList 的長度
* 它和 elementData.length 的區別是,size是實際元素個數,elementData數組裏,可能有4個實際元素,6個空元素,所以 elementData.length,代表了 elementData 的長度,它裏面包含了空元素。
*/
private int size;
2. ArrayList 的構造函數
2.1 指定容量構造函數:ArrayList(int initialCapacity)
/**
* 傳入初始容量,如果容量大於0,就將 elementData 初始化爲對應大小。如果等於0,那就使用空數組:EMPTY_ELEMENTDATA。如果小於0,就拋異常了。
* @param initialCapacity 傳入指定的容量
*/
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);
}
}
2.2 默認構造函數:ArrayList()
/**
* 如果不傳參數,那就使用空數組:DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
* 目前 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量爲0,要等到添加第一個元素時。它纔會初始化爲 DEFAULT_CAPACITY 的大小。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.3 傳入集合構造函數:ArrayList()
/**
* 傳入一個 Collection 集合,並調用 toArray() 方法將它裏面的內容初始化給 elementData。
* 然後再判斷元素個數是否爲0,如果爲0,就將 elementData 初始化爲 EMPTY_ELEMENTDATA 這個空數組
* @param c
*/
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;
}
}
這裏請注意!! 該構造方法中寫道:c.toArray might (incorrectly) not return Object[] (see 6260652)
意思是: c.toArray方法返回的可能不是 Object[] 類型,詳情見JDK bug編號 6260652
那麼這裏的 if 判斷是說,如果 elementData 不是 Object[] 的類型,就通過 copyOf 這個方法將不是 Object[] 的類型的 elementData 數組(它有可能是String[],int[]…不管它是什麼,暫時不管),轉換成 Object[] 類型。
這句話有點繞,簡單來說就是:如果 elementData 如果不是 Object[] 的類型,那就通過 copyOf 方法把它轉換成 Object[] 的類型
2.4 疑問:爲什麼 elementData 會可能不是 Object[] 的類型呢?
爲什麼這裏會有這一步操作呢?爲什麼 elementData 會不是 Object[] 的類型呢?下面舉例子解釋:
我們首先來看一下這個構造器裏面的內容,我們聚焦在第二層的這個 if 判斷上,它判斷了 elementData.getClass() 是否等於 Object[].class。那麼,elementData 是怎麼來的呢?構造器第一句就告訴我們了,他是 Collection 類型的參數 c 調用toArray方法初始化來的。 也就是說,這個 if 判斷的是 c.toArray 方法返回的類型是否是 Object[] 類型。
先理解上面這段說明,下面就舉一個栗子來分析說明:
// 1、首先使用工具類 Arrays 的 asList 方法,將數組轉換成 List。
List<String> myList = Arrays.asList("123456", "ABCDEF");
// 2、模擬進入構造器,構造器需要 Collection 類型的數據,爲什麼這裏傳 List 也可以呢?
//因爲 ArrayList 實現與 List 接口,List 接口又繼承於 Collection 類,所以 ArrayList 也算是 Collection 的子類。所以這裏傳 List 也 OK。
List<String> arrayList = new ArrayList<>(myList);
// 這裏的 myList 就是構造器裏面的 c 變量。 myList 爲實參, c爲形參。
// 根據上面的理解,如果 c.toArray方法返回的類型,也就是這裏的 myList.toArray 方法返回的類型不爲 Object[] 類型,那就做轉換動作,那麼 myList 的類型到底是什麼呢?
System.out.println(myList.toArray());
打印結果爲:
[Ljava.lang.String;@27c170f0
結果說明 c.toArray方法返回的類型 還真有不是 Object[] 類型的,所以構造器裏面存在這個if判斷操作。
這個時候,可能有同學要問了:
- 爲什麼 myList.toArray 方法返回的不是 Object[] 類型呢???
- 在例子中 myList 已經是一個 List 了,我爲什麼不直接用它呢,爲什麼還要把它當作參數再創建新的ArrayList呢,我直接用
myList 不就得了嗎?
2.5 解答問題1:爲什麼 myList.toArray 方法返回的不是 Object[] 類型呢???
首先看例子中,我們是使用工具類 Arrays 的 asList 方法,將字符串數組轉換成 List。那麼我們點進 asList 方法看一下源碼:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new Arrays.ArrayList<>(a);
}
可以看見它只不過是 new 了一個 Arrays.ArrayList<>(a) 對象而已。
那麼這個 Arrays.ArrayList<> 是個什麼東西呢?它其實就是 Arrays 類裏面的一個內部類,它就在 asList 方法的下面,源碼中他們緊挨着。部分代碼如下:
Arrays.ArrayList<> 的源碼
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
......
也就是說 Arrays.asList 方法返回的是 Arrays 的一個內部類,它繼承於 AbstractList,所以它返回的並不是 java.util.ArrayList。
現在知道 Arrays.asList 方法返回的是什麼之後,再來看爲什麼它調用 toArray() 方法之後,返回的不是 Object[] 類型。
現在將目光聚焦在我貼出來的 Arrays.ArrayList 這個內部類的部分源碼,看最後一個方法,這是啥!!!!!其實他重寫了父類的toArray()方法,所以當我們在使用 Arrays.asList 方法創建出來的對象的 toArray 方法時,調用的是他自己重寫的方法!!
現在再看他是如何重寫的,它是 clone 了一份 成員變量 a 的數據,a 是我們創建對象時傳進來的,一直往上追,可以發現 a 裏面就是我們用例中傳的字符串數組,如果我們用例中寫的兩個int類型的數據,那麼此時的 a 就會是這個int數組。所以 toArray 方法其實返回的是一個數組,它並不是一個真正的List。
現在明白爲啥例子中打印出來是字符串數組類型的,而不是 Object[] 類型了吧。
2.6 解答問題2:在例子中 myList 已經是一個 List 了,我爲什麼不直接用它呢,爲什麼還要把它當作參數再創建新的ArrayList呢,我直接用 myList 不就得了嗎?
滿足願望,那就先使用 myList 增加一個元素試試:
myList.add("HyugaNeji");
結果顯示:
java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at AsListTest.main(AsListTest.java:25)
結果是拋異常了,原因是 myList 的類型是 Arrays.ArrayList 類型的,它自己沒有實現 add 方法,但是它繼承的父類 AbstractList 實現了,所以這裏調用 add ,是調用到了它的父類的 add 方法,而它的父類是實現了 add 方法的,其內容如下:
public boolean add(E e) {
add(size(), e);
return true;
}
發現這個方法又調用了 另一個 add 方法,繼續往下看,在 147 行的位置發現了這個方法:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
結果已經很明顯了,它的實現方法就是拋異常…
所以,Arrays.asList 創建的所謂的List,是不可以使用 add、set、remove這些方法的,既然這些方法都不能用,我要他有何用,所以纔會使用它作參數再創建新的ArrayList。所以纔有了構造器裏面的那段代碼。
至於,Arrays.ArrayList爲什麼不直接返回一個 java.util.ArrayList 的原因,我現在暫時還沒搞清楚,可能是設計模式層面的東西,因爲它現在這樣實現,不就是適配器模式嗎?
一口氣學習了這麼多,可累壞了。歇會兒
3. 保證數組容量安全的核心輔助方法
先介紹幾個保證數組容量安全的核心輔助方法
3.1 ensureCapacityInternal 方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
ensureCapacityInternal 方法的意思是 確保 ArrayList 內部容量的意思。如果容量不夠裝了就進行擴容,確保容量。
它的實現是調用了兩個方法,它通過調用 calculateCapacity 方法,拿到返回值,再傳給 ensureExplicitCapacity 方法。下面我們先看 calculateCapacity 方法做了什麼。
3.2 calculateCapacity 方法
calculateCapacity 方法,看名字就知道它是 計算容量的,它的目的是返回 ArrayList 要存放數據的最小的目標容量。
它通常在 add 數據的時候被使用到,參數 minCapacity 的意思是存放數據需要最小的容量,它是: ArrayList 的實際存放元素個數 + 新增的個數
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 的引用是否是相同的,也就是判斷是否調用了無參構造器。
因爲如果調用了無參構造器,那麼 elementData 的容量 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量 == DEFAULT_CAPACITY(原因詳見構造器函數)
如果是相同的,那麼 elementData 的初始容量就是 DEFAULT_CAPACITY,所以比較 DEFAULT_CAPACITY 和 minCapacity ,誰大返回誰。
換句話說就是:如果調用了無參構造器,那麼最小容量最小就是10。這個10容量,可能是幾個實際元素 + 幾個空元素;
如果不是調用的無參構造器,那麼就直接返回 minCapacity。也就是說:我新增元素的個數 + 數組中已有的元素個數 = 最小容量minCapacity,那你最少的給我準備 minCapacity 個數的容量,纔夠裝我的數據。
3.3 ensureExplicitCapacity 方法
ensureExplicitCapacity 方法決定了 ArrayList 要不要擴容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 首先累加修改次數
- 如果我實際要存放數據的個數(minCapacity) 減去 elementData.length 大於0,
也就是說,我要存放數據的個數大於數組的長度,這個數組裝不下了,就需要調用 grow() 方法 擴容了。
3.4 允許最大數組長度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
減8的目的是爲了留位置存放數組的長度,因爲數組自己不能計算長度,需要留個位置記錄一下
3.5 擴容的方法 grow (擴容核心函數)
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow 擴容方法,傳入參數(minCapacity) 告訴這個方法,我需要存放的數據的個數,也就是最小的容量,你的容量最小得等於我的個數,不然就不夠裝啊。
- 首先,算出數組舊的容量,賦值給 oldCapacity 變量
- 然後計算出 舊容量的1.5倍,在賦值給新容量 newCapacity。這裏 oldCapacity >> 1 相當於除以2,在加上原來的就是1.5倍了。JDK1.6的做法是:int newCapacity = (oldCapacity*3)/2+1,這裏位運算的速度是要比整除效率高。我這是JDK1.8。
- 接着判斷,如果 newCapacity - minCapacity 如果小於 0 表示新容量比最小我要求容量還要小,也就是擴容後你還不夠裝的話,那就使用我要求的最小容量吧。將最小容量的值賦值給新容量變量。
- 再接着判斷,如果 newCapacity - MAX_ARRAY_SIZE 大於 0 表示 新容量比允許最大數組長度都還要大,那咋辦。那就只有請出我的大寶貝了,啊,呸…請出巨大容量函數:hugeCapacity(minCapacity)。並將它的返回值賦值給新容量。
- 最後,再將老數組的裏面的數據拷貝到新數組裏面去。
下面看看 巨大容量函數:hugeCapacity(minCapacity) 做了啥。
3.6 hugeCapacity 方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 首先,該函數判斷了 minCapacity 是否是小於 0,如果是,那就表示內存溢出啦,直接拋錯誤。因爲int是有範圍的,超出了整個範圍之後,就會變成一個負數。
- 然後判斷,我所需要的容量(minCapacity),是否是大於 MAX_ARRAY_SIZE,如果是那就返回 Integer.MAX_VALUE:整型的最大值 2^{31}-1。否則就返回 MAX_ARRAY_SIZE
所以 ArrayList 擴容的核心思想是:擴容原來數組長度的1.5倍,然後再將老數組裏面的數據拷貝到新數組中
4. 平時常用的方法
4.1 添加元素方法:add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 首先它調用了 ensureCapacityInternal 方法確保容量,檢查是否需要擴容,關於這個方法的詳細解釋和調用鏈在前面已經學習過了,這裏就不多解釋了。
- 然後進行賦值
4.2 添加元素到指定位置方法:add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 首先 調用 rangeCheckForAdd 函數,檢查指定位置是否越界,這個函數只有 add 和 addAll 的時候使用。
- 確保容量,檢查是否需要擴容
- 把指定索引位置後的元素都往後挪一位;
- 在指定索引位置放置插入的元素;
- 累加實際存放元素個數。
4.3 將指定集合的數據添加到之前的集合中:addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- 首先,將參數 c 的數據拷貝到數組 a 中
- 算出 a 的長度,也就是這次於要添加數據的個數
- 確保容量,檢查是否需要擴容
- 把數組a中的元素拷貝到elementData的尾部;
- 累加實際存放元素個數。
4.4 獲取指定索引位置的數據:E 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));
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
首先檢查索引是否越界,這裏只檢查是否越上界,如果越上界拋出IndexOutOfBoundsException異常,如果越下界拋出的是 ArrayIndexOutOfBoundsException異常。
然後在返回指定索引位置處的元素;
4.5 移除指定位置的元素: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;
}
- 檢查索引是否越界;
- 獲取指定索引位置的元素;
- 如果刪除的不是最後一位,則其它元素往前移一位;(計算後 numMoved 如果等於0,那麼表示刪除的最後一位)
- 將最後一位置爲null,方便GC回收;
- 返回刪除的元素。
注意:ArrayList刪除元素的時候並沒有縮小容量。
4.5 刪除指定對象:remove(Object o)
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
}
- 如果指定對象爲 null的話,單獨處理,ArrayList是可以存儲null 的。 兩段處理邏輯本質並沒有變化:找到第一個等於指定元素值的元素
- 調用 fastRemove方法 快速刪除;
fastRemove(int index)相對於remove(int index)少了檢查索引越界的操作,並且不會返回已刪除的值。
4.6 求兩個集合的交集:retainAll(Collection<?> c)
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
- 遍歷elementData數組;
- 如果元素在c中,則把這個元素添加到elementData數組的w位置並將w位置往後移一位;
- 遍歷完之後,w之前的元素都是兩者共有的,w之後(包含)的元素不是兩者共有的;
- 將w之後(包含)的元素置爲null,方便GC回收;
4.7 根據指定集合,刪除原數組中與集合數據相同的數據:removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
與retainAll(Collection<?> c)方法類似,只是這裏保留的是不在c中的元素。
4.8 清楚所有元素:clear()
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
- 首先記錄修改次數
- 循環 size ,將 elementData 裏面的元素設置爲 null,方便 GC 工作。
- 最後將 size 設置爲 0。
5. 解答上面疑問:transient Object[] elementData 爲何 elementData 要設置成 transient
transient 關鍵字的意思是不讓序列化該關鍵字修飾的內容
而 ArrayList 它是實現了 java.io.Serializable 接口,這表示它可以被序列化,但是真正存儲數據的數組卻修飾成了不讓序列化。那麼這麼做有什麼意義呢?
原因是:ArrayList 源碼裏面有 writeObject() 和 readObject() 兩個方法,這兩個方法聲明爲private,在只有再 java.io.ObjectStreamClass#getPrivateMethod() 方法中通過反射獲取到 writeObject() 這個方法
這樣做的目的是:爲了自己控制序列化的方式! 因爲 elementData 是一個緩存數組,它通常會預留一些容量,等容量不足時再擴充容量, 所以ArrayList的設計者將elementData設計爲transient,然後在writeObject方法中手動將其序列化,並且只序列化了實際存儲的那些元素,而不是整個數組,這樣減少了空間佔用。
6. 總結
- ArrayList內部使用數組存儲元素,當數組長度不夠時進行擴容,每次擴容1.5倍空間,remove 和 clear 不會讓 ArrayList進行縮容
- ArrayList添加元素到中間比較慢,因爲要挪動元素
- ArrayList從中間刪除元素也比較慢,因爲要挪動元素
- ArrayList支持隨機訪問,通過索引訪問元素很快。
技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。