原始碼分析篇--Java集合操作(6)順序表的擴容原理
2.6.4 順序表的擴容原理之add()方法的實現原理
add()方法用於將元素動態新增到ArrayList容器中。將資料儲存在list後,我們可以通過該list進行CRUD操作。我們知道,list(列表)的底層是由陣列設計的,因此,陣列具備了哪些優缺點,list就具備了哪些優缺點。我們知道,連結串列與陣列是兩種不同的資料結構,資料結構可以分為線性結構和非線性結構,線上性結構中,儲存方式又分為連續儲存(陣列)和離散儲存(連結串列),例如棧和佇列都是線性結構常見的應用。我們可以將陣列簡單地理解為一種線性表資料結構(線性表是動態的),因為陣列一旦定義了,其長度就不可以更改了,也就是不可以做增刪操作,但可以做修改和查詢操作。所以List其實就是線性表。線性表是一種可以在任意位置插入和刪除元素,由n個同類型元素組成的線性結構。主要包括順序表,單鏈表,迴圈單鏈表,雙向連結串列和模擬連結串列。應用比較廣泛的是順序表和單鏈表。我們可以將線性表理解為動態陣列或連結串列。在Java中,ArrayList(列表)就是一種動態陣列,也就是資料結構中線性表的一種—順序表。順序表支援插入元素,刪除元素,取得元素等操作。下面介紹add()方法的實現:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
原生的add方法有一個E型別的引數,返回布林型別,E型別是一個範型,這意味著你可以新增任意引用型別的元素給list。例:
//String型別 List<String> listString = new ArrayList<String>(); String string = "hello world!"; listString.add(string); //Integer型別 List<Integer> listInt = new ArrayList<Integer>(); Integer integer = 1; listInt.add(integer); //Object型別 List<Object> listObject = new ArrayList<Object>(); //Integer、String可以看作Object型別 listObject.add(integer); listObject.add(string); //Object[]可以看作Object型別 Object[] objectArr = {1,"hello",'a'}; listObject.add(objectArr); //陣列 List<String[]> listStringArr = new ArrayList<String[]>(); String[] stringArr = {"h","e","l","l","o"}; listStringArr.add(stringArr); //List型別 List<List<String>> listList = new ArrayList<List<String>>(); listList.add(listString); //Map型別 List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>(); Map<String,Object> map = new HashMap<String,Object>(); map.put("a", objectArr); listMap.add(map); //List-Map型別 List<List<Map<String,Object>>> listListMap = new ArrayList<List<Map<String,Object>>>(); listListMap.add(listMap); 考題 下列哪些型別的資料可以作為List的元素?(BCD) A.double d = 1.0d B.List<String> C. Map<String,Object> D.List<Map<String,List<String>>> 說明:double是基本資料型別,因此不能做List的元素。
在add方法中,實際上是在給一個名為elementData陣列新增元素並擴容。那麼該陣列是如何來進行擴容的呢?這裡需要提到一個有關陣列拷貝的方法,我們先從原始碼的角度進行分析,
有關list中的add方法如下所示。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
在add()方法中,通過elementData陣列來新增元素,通過ensureCapacityInternal()來為elementData陣列進行擴容,ensureCapacityInternal()是一個私有方法,僅供當前類呼叫,從方法名可以看出,這個方法可理解為確定容器容量的內部(internal)方法,由於陣列初始化時為空,此時,初始化容初始化的容量為10,但需要注意的是,這裡所指的初始化容初始化的容量為10的含義是在使用預設構造方法的情況下首次呼叫add()後初始化的容量,而不是使用無參構造器建立一個list的容量,我們可以從下面的程式碼來驗證這一點:
List<String> listString2 = new ArrayList<String>();
Class<?> c11 = listString2.getClass();
Field f = null;
Object[] value = null;
try {
f = c11.getDeclaredField("elementData");
//設定私有屬性可操作
f.setAccessible(true);
value = (Object[]) f.get(listString2);
System.out.println("\n 陣列的長度:"+value.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
listString2.add("a");
try {
f = c11.getDeclaredField("elementData");
//設定私有屬性可操作
f.setAccessible(true);
value = (Object[]) f.get(listString2);
System.out.println("陣列的長度:"+value.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**output:
陣列的長度:0
陣列的長度:10
**/
因此,需要判斷elementData是否為空元素陣列,我們發現,在ArrayList無參構造器中,有一個表示式: this.elementData = EMPTY_ELEMENTDATA;因此無參構造器的陣列初始化等價於EMPTY_ELEMENTDATA,從上面的結果我們看到,容器對應的陣列是在add()方法執行後才初始化容量的,因為在呼叫的add()方法中有一句關鍵的程式碼:int minExpand = (elementData !=EMPTY_ELEMENTDATA)? 0 : DEFAULT_CAPACITY;該程式碼的作用是,如果你選擇了預設構造器:public ArrayList(),那麼elementData等價於EMPTY_ELEMENTDATA,此時通過int minExpand = (elementData !=EMPTY_ELEMENTDATA)? 0 : DEFAULT_CAPACITY;可以得知容器的初始化容量為DEFAULT_CAPACITY,也就是10個記憶體空間。ensureCapacityInternal 除了判斷是不是初始化的容器陣列外,還有另一個作用:通過呼叫ensureExplicitCapacity()來擴充容量,ensureCapacityInternal方法的實現如下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
ensureExplicitCapacity()方法中有一個modCount,這個用來記錄擴容的次數,當size+1大於elementData.length時,這是擴容的臨界條件,因此呼叫grow()方法來進行擴容:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
擴容的演算法如下所示:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容1.5仍小於容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
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就是當前新增一個元素後的size,假設原容量是10,當前容量是11,因此需要擴容,根據擴容演算法我們知道,首先會擴容為原來的1.5倍,也就是15,顯然15>11,如果原容量為1,那麼原容量是1,當前容量是2,因此需要擴容,擴容後仍然是1,因此執行if (newCapacity - minCapacity < 0)該語句,因此第一次擴容為2;當然擴容是有一個範圍的,那就是必須在MAX_ARRAY_SIZE範圍內,否則就呼叫hugeCapacity()方法進行處理。但是,對於有參構造方法不一樣,如果是有參構造方法,那麼陣列的容量會在初始化後就有一個與引數等量的儲存空間。我們可以驗證一下構造器與擴容的關係:
List<Integer> listInteger = new ArrayList<Integer>(12);
Class<?> c12 = listInteger.getClass();
Field ff = null;
Object[] value2 = null;
try {
ff = c12.getDeclaredField("elementData");
//設定私有屬性可操作
ff.setAccessible(true);
value2 = (Object[]) ff.get(listInteger);
System.out.println("\n 陣列的長度:"+value2.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**output:
陣列的長度:12
*/
List<Integer> listInteger = new ArrayList<Integer>(12);
listInteger.add(1);
listInteger.add(2);
listInteger.add(3);
listInteger.add(4);
Class<?> c12 = listInteger.getClass();
Field ff = null;
Object[] value2 = null;
try {
ff = c12.getDeclaredField("elementData");
//設定私有屬性可操作
ff.setAccessible(true);
value2 = (Object[]) ff.get(listInteger);
System.out.println("\n 陣列的長度:"+value2.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**output:
陣列的長度:12
*/
從上面的例子我們可以看出,如果你建立了一個持有初始化容量引數的構造器,那麼在呼叫add()方法時,只要保持新增的元素總數在該引數因子內,即使是沒有新增任何元素,和建立容器物件時,底層的容器陣列的儲存空間是保持一致的。但是如果當元素超過了容器的儲存空間,也會按照1.5倍的原有容量來進行擴容:
List<Integer> listInteger = new ArrayList<Integer>(6);
listInteger.add(1);
listInteger.add(2);
listInteger.add(3);
listInteger.add(4);
listInteger.add(1);
listInteger.add(2);
listInteger.add(3);
Class<?> c12 = listInteger.getClass();
Field ff = null;
Object[] value2 = null;
try {
ff = c12.getDeclaredField("elementData");
//設定私有屬性可操作
ff.setAccessible(true);
value2 = (Object[]) ff.get(listInteger);
System.out.println("\n 陣列的長度:"+value2.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**output:
陣列的長度:9
9=6+6/2
*/
List<Integer> listInteger = new ArrayList<Integer>(5);
listInteger.add(1);
listInteger.add(2);
listInteger.add(3);
listInteger.add(4);
listInteger.add(1);
listInteger.add(2);
Class<?> c12 = listInteger.getClass();
Field ff = null;
Object[] value2 = null;
try {
ff = c12.getDeclaredField("elementData");
//設定私有屬性可操作
ff.setAccessible(true);
value2 = (Object[]) ff.get(listInteger);
System.out.println("\n 陣列的長度:"+value2.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**output:
陣列的長度:7
7=5+5/2 =5+2
*/
為什麼會是這個結果呢?我們通過原始碼來分析一下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
當呼叫ensureCapacityInternal()方法時,elementData[size++] = e;還沒執行,這意味著需要擴容,擴容的條件是:minCapacity - elementData.length > 0,也就是傳入的元素的個數+1大於陣列的容量時就需要對該陣列進行擴容了,由於擴容的原理還是增大原來的1.5倍,因此就可以解釋上面的結果了。
例
ArrayList擴容發生在下面(B)方法中。
A.public ArrayList() B.add()
從上面我們可以看出,ArrayList擴容發生在add()方法中。
例
List list = new ArrayList(5);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
則上面的list中的elementData陣列擴容了幾次?
答案:0次,因為發生擴容的條件是if (minCapacity - elementData.length > 0),由於傳入構造器的引數是5,因此elementData初始化大小為5,根據elementData[size++]=e可知,size最終大小為4,4<5,顯然並沒有擴容。
例
List<Integer> listInteger2 = new ArrayList<Integer>(4);
listInteger2.add(1);
listInteger2.add(2);
listInteger2.add(3);
listInteger2.add(4);
listInteger2.add(5);
for(int i:listInteger2){
System.out.print(i+" ");
}
System.out.println();
請輸出上面程式的結果。
因為發生擴容的條件是if (minCapacity - elementData.length > 0),由於傳入構造器的引數是4,因此elementData初始化大小為4,根據elementData[size++]=e可知,size最終大小為5,5>4,顯然擴容了,擴容後的elementData大小為4+2=6。所以發生了一次擴容,並且size最終為5,因此會遍歷出全部的元素:1 2 3 4 5。
注意:反射方法中getDeclaredFields用於獲取本類所有的屬性,而getFields只用於獲取本類的公有屬性。這兩個方法都不能獲取繼承自父類的屬性。
適用範圍<訪問許可權範圍越小,安全性越高>
訪問許可權 類 包 子類 其他包
public ∨ ∨ ∨ ∨ (對任何人都是可用的)
protect ∨ ∨ ∨ × (繼承的類可以訪問)
default ∨ ∨ × × (包訪問許可權,即在整個包內均可被訪問)
private ∨ × × × (除型別建立者和型別的內部方法之外的任何人都不能訪問的元素)