ArrayList原始碼分析之 add 方法
在Java程式設計中,常常需要集中存放多個數據,從傳統意義上講,陣列是我們的一個很好的選擇,前提是我們事先已經明確知道我們將要儲存的物件的數量。一旦在陣列初始化時指定了這個陣列長度,這個陣列長度就是不可變的,如果我們需要儲存一個可以動態增長的資料(在編譯時無法確定具體的數量),
List 這個集合類是便為我們提供了相當於動態陣列的功能。這個類中add方法尤為重要。
1 目標
本次原始碼分析的目標是深入瞭解 List類中 add 方法的實現機制。
2 分析方法
首先編寫測試程式碼,然後利用 Intellij Idea 的單步除錯功能,逐步的分析其實現思路。
測試程式碼如下:
List<String> mList=new ArrayList<String>();
mList.add("張三");//斷點
mList.add("李四");
mList.add("王五");
mList.add(1,"趙六");//斷點
3 分析流程
點選除錯按鈕,開始分析流程。
3.1 建構函式
首先進行的是建構函式的分析,點選 Shift+F7進入建構函式實現。
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
點選進入後,我們看到的是以上程式碼,這裡疑惑就來了,這個elemendata代表什麼呢?這個DEFAULTCAPACITY_EMPTY_ELEMENTDATA又是什麼意思呢?
我們可以在定位在elemenData處按 Ctrl+B進入檢視做進一步分析。
transient Object[] elementData;
通過上述程式碼我們知道,elemenData是一個Object型的陣列。那為什麼會定義成Object型的呢?我們可以想一下,如果是String,int或是其他型別的,那麼存放的資料是不是就受到了限制了呢?而Object型的就能保證我們可以將任何型別的資料存放進去。可以看到,
我們都知道一個物件只要實現了Serilizable介面,這個物件就可以被序列化,java的這種序列化模式為開發者提供了很多便利,我們可以不必關係具體序列化的過程,只要這個類實現了Serilizable介面,這個類的所有屬性和方法都會自動序列化。然而在實際開發過程中,我們常常會遇到這樣的問題,這個類的有些屬性需要序列化,而其他屬性不需要被序列化,打個比方,如果一個使用者有一些敏感資訊(如密碼,銀行卡號等),為了安全起見,不希望在網路操作(主要涉及到序列化操作,本地序列化快取也適用)中被傳輸,這些資訊對應的變數就可以加上transient關鍵字。換句話說,這個欄位的生命週期僅存於呼叫者的記憶體中而不會寫到磁盤裡持久化。
同理我們來看DEFAULTCAPACITY_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 = {};
可見這是一個Object型的常量陣列,並將其賦了空值。到這裡我們就能知道構造方法裡是將elemenData陣列初始化為空。
3.2 booleanadd(E e)方法
接下來我們分析add( E e)方法的實現機制。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
確定內部容量是否夠了,size是陣列中資料的個數,因為要新增一個元素,所以size+1,先判斷size+1的這個個數陣列能否放得下,就在這個方法中去判斷陣列.length是否夠用了。
從上述程式碼可以看到,這裡呼叫了ensureCapacityInternal方法,我們來看看其實現機制
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
由此我們可以看出這是一個確定內部容量的方法,這裡我們先定位在DEFAULT_CAPACITY上Ctrl+B進去可以看到其值為10;首先判斷初始化的elementData是不是空的陣列,也就是沒有長度因為如果是空的話,minCapacity=size+1;其實就是等於1,空的陣列沒有長度就存放不了,所以就將minCapacity變成10,也就是預設大小,但是在這裡,還沒有真正的初始化這個elementData的大小。
我們繼續按F7除錯下一步,將會執行ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
minCapacity如果大於了實際elementData的長度,那麼就說明elementData陣列的長度不夠用,不夠用那麼就要增加elementData的length。有的同學可能會疑惑minCapacity到底是什麼呢?我們來分析一下:
1) 由於elementData初始化時是空的陣列,那麼第一次add的時候,minCapacity=size+1;也就minCapacity=1,在上一個方法(確定內部容量ensureCapacityInternal)就會判斷出是空的陣列,就會將minCapacity=10,到這一步為止,還沒有改變elementData的大小。
2) elementData不是空的陣列了,那麼在add的時候,minCapacity=size+1;也就是minCapacity代表著elementData中增加之後的實際資料個數,拿著它判斷elementData的length是否夠用,如果length不夠用,那麼肯定要擴大容量,不然增加的這個元素就會溢位。
F7除錯下一步執行grow方法,arrayList核心的方法,能擴充套件陣列大小的真正祕密。我們下一節詳細分析該方法。
3.3 grow(int minCapacity)方法
按住 Shift+F7進入該方法的實現程式碼。
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //將擴充前的elemenData大小給oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity的大小就是1.5倍的oldCapacity,相當於將elemenData之前的大小擴充為原來的1.5倍。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//這段程式碼判斷將elemenData大小擴充1.5倍過後與minCapacity作比較,在§3.2節我們已經知道minCapacity預設值為10。判斷成立便將minCapacity賦值給newCapacity,在這裡就是真正的初始化elemenData的大小了。這段程式碼適應於elemenData陣列為空的時候,elementData.length=0,則oldCapacity=0,newCapacity=0,所以該判斷成立。
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);
}
minCapacity作比較,在§3.2節我們已經知道minCapacity預設值為10。判斷成立便將minCapacity賦值給newCapacity,在這裡就是真正的初始化elemenData的大小了。這段程式碼適應於elemenData陣列為空的時候,elementData.length=0,則oldCapacity=0,newCapacity=0,所以該判斷成立。
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);
}
if (newCapacity- minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity- MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//這裡有個MAX_ARRAY_SIZE,可是MAX_ARRAY_SIZE代表什麼呢?
按住Ctrl+B檢視原始碼:
/**
* 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;
根據註釋我們瞭解到MAX_ARRAY_SIZE代表最大容量限制。所以程式碼含義是如果newCapacity超過了最大容量限制的話就呼叫hugeCapacity方法,將能給的最大值給newCapacity。hugeCapacity方法又是怎麼一回事呢?我們下一節再來介紹。
elementData= Arrays.copyOf(elementData,newCapacity);//最後copy陣列,改變陣列容量。
3.4 hugeCapacity()方法
按住 Shift+F7進入該方法的實現。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
從上述程式碼可以看出,hugeCapacity方法用於確定elemenData陣列的最大容量。如果minCapacity > MAX_ARRAY_SIZE說明擴容時擴得過大,則返回Integer.MAX_VALUE,否則返回MAX_ARRAY_SIZE。在上一節已經說過MAX_ARRAY_SIZE的含義,那MAX_ARRAY_SIZE和Integer.MAX_VALUE的值具體又是多少呢?
按住Ctrl+B檢視原始碼:
@Native public static final int MAX_VALUE = 0x7fffffff;
3.5void add(int,E)方法
按住 Shift+F7進入該方法的實現程式碼:
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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++;
}
此方法用於在特定位置將元素新增進集合,也就是插入元素。先呼叫rangeCheckForAdd方法檢查插入的位置是否合理,然後呼叫ensureCapacityInternal方法將index之後的元素都往後移動一位(具體分析看上面的小節),並在目標位置上將要插入的元素存放進elemenData陣列。最後陣列大小加一。
4 總結
本文分析了List類的 add 方法,包括在末尾新增元素和在指定位置新增元素。通過分析我們知道List集合就相當於一個elemenData陣列,該陣列可以實現自動擴容。當我們呼叫add方法的時候實際上的函式呼叫順序為:
後面兩個方法並不是每次呼叫add方法時,當新增進的元素數量大於elemenData預設容量時會呼叫grow方法進行擴容,而當擴容過大時會呼叫hugeCapacity方法進行最大容量限制。
對比一下傳統的陣列,在使用陣列時我們需要事先固定好其大小,然而在實際開發中我們往往是不能預測究竟會存放多少資料進去的,當我們需要改變陣列大小時,又要回過頭開修改程式碼,這豈不是徒增麻煩。而List集合就避免了這種麻煩,給開發人員與維護人員提供了極大地便利,所以在實際開發中被人們廣泛使用。瞭解其實現機制是很有意義的。