1. 程式人生 > >原始碼分析篇--Java集合操作(6)順序表的擴容原理

原始碼分析篇--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 ∨ × × ×    (除型別建立者和型別的內部方法之外的任何人都不能訪問的元素)