1. 程式人生 > >#Java集合框架--ArrayList容器的構造器

#Java集合框架--ArrayList容器的構造器

ArrayList類實現了List介面,它繼承自AbstractList抽象類,繼承機構如下所示:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
其中,AbstractList抽象類的繼承結構如下所示:
public abstract class AbstractList extends AbstractCollection implements List
其中,AbstractCollection抽象類的整合結構如下所示:
public abstract class AbstractCollection implements Collection
由於ArrayList類實現了List介面,因此它具有List介面的全部方法。下面我們來介紹開發中常見的幾個方法。

  1. ArrayList()構造方法
    預設的構造方法,可以通過new ArrayList()的方式獲得一個list物件,在開發中比較常見。
  2. ArrayList(int initialCapacity)
    該構造器需要傳入一個初始化容量。這個構造方法自己指定ArrayList的初始化長度。當初始化長度小於0的時候,丟擲異常。在開發中,鼓勵使用初始化list容器的構造器,這是因為list的底層是陣列不斷擴容的過程,預設第一次擴容大小是10,後面每一次擴容是前面擴容的1.5(3/2)倍;
    //容量增大為原來的3/2倍:7(111)>>1 => 011(3) 7/2=3;移1位相當於除以2。
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //陣列拷貝,並擴容
    elementData = Arrays.copyOf(elementData, newCapacity);

經典考題
ArrayList list = new ArrayList(20)擴容了幾次?(A)
A.0 B.1 C.2 D.3
因為該構造方法是直接初始化為容量20的容器,沒有擴容的過程,底層程式碼如下所示。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}

為什麼說引數型建構函式在一定條件下要比無參構造方法效率要快呢?
預設情況下,無參構造方法的擴容量為10,當入參大於10時,由公式新容量 = 舊容量/2+舊容量+1可知,新增同樣多的元素,初始化因子越大,擴容的次數越少,效率越高。但是並不是擴容因子越大就越好,下面的案例說明了這一點:
List list5 = new ArrayList();
long start1 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list5.add(i);
}
long end1 = System.currentTimeMillis();
System.out.println(“list5:”+(end1-start1));//2394

List list6 = new ArrayList(5);

long start2 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list6.add(i);
}
long end2 = System.currentTimeMillis();
System.out.println(“list6:”+(end2-start2));//2911

List list7 = new ArrayList(20);
long start3 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list7.add(i);
}
long end3 = System.currentTimeMillis();
System.out.println(“list7:”+(end3-start3));//2293
從上面的結果我們可以看出來,在一定範圍內,有參建構函式的擴容量因子越大,擴容次數減少,效率越高。當然,一般情況下,擴容量選擇20比較合適,這取決於機器的效能。

擴容公式:
新容量 = 1.5*舊容量
考題2
下面的程式碼中,經歷了幾次擴容?©
List list7 = new ArrayList();
long start3 = System.currentTimeMillis();
for(int i = 0;i<31;i++){
list7.add(i);
}

A.1 B.2 C.3 D.4
說明:預設情況下,初始化容量為10,以後按1.5原有的容量進行擴容。因為31>10,因此,一定發生了擴容,設經歷了至少x次擴容,那麼1.5^x10>=31,由此可以估算出x=3。擴容過程為:101.5=>15,151.5=>22,221.5=33。那麼為什麼說預設情況下,初始化容量為10,以後按1.5原有的容量進行擴容呢?我們可以看看底層的程式碼:
//預設的容量
private static final int DEFAULT_CAPACITY = 10;
//在add中呼叫ensureCapacityInternal()進行擴容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//確認容量minCapacity
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}
//Explicit明確的,確認容器的容量,通過判斷當前的元素個數(包括新增)與陣列的長度作對比,如果元素個數大於陣列的長度,那就呼叫grow方法進行擴容:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//擴容方法,通過int newCapacity = oldCapacity + (oldCapacity >> 1);這句可以看出,預設情況下,初始化容量為10(通過minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);該句可以看出來,因為初始條件下,size+1=1<10,因此minCapacity= DEFAULT_CAPACITY=10),以後按1.5原有的容量進行擴容。
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);
}

考題3
下面的程式碼中,經歷了幾次擴容?(B)
List list7 = new ArrayList(20);
long start3 = System.currentTimeMillis();
for(int i = 0;i<31;i++){
list7.add(i);
}

A.1 B.2 C.3 D.4
解:我們看底層的有參構造方法為:
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: “+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
可知,此時size= initialCapacity;
利用公式新容量 = 1.5舊容量可得:201.5=30,30*1.5=45;因此一共經歷了兩次擴容。我們可以通過反射來檢驗擴容的陣列elementData:
List list7 = new ArrayList(20);
for(int i = 0;i<31;i++){
list7.add(i);
}
System.out.println(list7);
Class<? extends List> cl = (Class<? extends List>) list7.getClass();
try {
Field f = cl.getDeclaredField(“elementData”);
f.setAccessible(true);
try {
Object[] o = (Object[])f.get(list7);
for(Object o1:o){
System.out.print(o1+” ");
}
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 null null null null null null null null null null null null null null,由此我們可以看出來,初始化容量因子越大,擴容次數越少,但是,每次擴容開闢所需的記憶體空間也會比較大,elementData陣列有時會發生大量的記憶體空間沒有被佔位的現象,因此,初始化容量並不是越大越好,一是可能浪費記憶體空間,二是開闢記憶體空間也會影響效率,因此初始化容量越大也會出效率低下的現象,一般而言,以20個初始化容量為最佳。

考題4
在初始化集合時,初始化容量因子數越大越好(錯)。
考題5
ArrayList繼承自下面哪兩個類?(A)
A、AbstractList和AbstractCollection B、List和AbstractCollection
C、AbstractList和List D、Collection和Object
選A。我們來看一下ArrayList的繼承關係:
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable

public abstract class AbstractList extends AbstractCollection implements List
public abstract class AbstractCollection implements Collection
從上面的繼承關係我們可以看出來,ArrayList直接繼承自AbstractList 抽象類,實現了List介面,AbstractList又繼承自AbstractCollection抽象類,實現了List介面,而AbstractCollection實現了Collection介面。因此,ArrayList類繼承的類 有:AbstractList、AbstractCollection和Object;ArrayList實現了List介面,綜上所述,答案為A。

3)ArrayList(Collection<? extends E> c)
在介紹這個構造器之前,我們先來了解一下範型符號的使用。
在建立集合物件的時候,要保證前後的泛型一致,或者建立集合時與前面的範型保持某種關係,如果不一致或不存在關係在編譯時就會出錯。例如,下面這句就會編譯出錯:
List listTest = new ArrayList();
為什麼會編譯出錯呢?
這是因為ArrayList雖然是List的實現類,String是Object的實現類,但是ArrayList不是List的子類或實現類,因此會發生編譯錯誤。我們來模擬一下這個過程:
package com.yx.yzh.test;
public interface Test{}

package com.yx.yzh.test;
public class Test4 implements Test{
public static void main(String[] args) {}
}
我們看到,Test與Test4的範型都是T,因此在建立Test4的物件時,需要保持前後的範型的型別一致,否則會發生編譯錯誤,例如,下面兩種情況會發生編譯錯誤:
Test test = new Test4();
Test test2 = new Test4();
需要保持一致:
Test test3 = new Test4();
我們再來看一下繼承關係中的範型:
package com.yx.yzh.test;
public class Test1 {}

package com.yx.yzh.test;
public class Test5 extends Test1 {
public static void main(String[] args) {
//編譯錯誤
//Test1 test = new Test5();
//編譯錯誤
//Test1 test2 = new Test5();
//正確
Test1 test = new Test5();
}
}

但是,要注意當兩種或以上的範型遇上時,需要將範型提到第一個型別的範型上,且其中之一的範型與實現類或繼承類的範型保持一致:
package com.yx.yzh.test;
public class Test1 {}

package com.yx.yzh.test;
public class Test3<E, E2> extends Test1 {
public static void main(String[] args) {
Test1 t = new Test3<Object,String>();
}
}
E2範型需要一一對應,因此Test1與Test3的第二範型保持一致,均為String型別,剩下的範型沒有一一對應關係,因此可為任意型別。這裡涉及到了一個概念:向上轉型。向上轉型是指父類或介面的引用指向子類或實現類。也就是說使用父類或介面來接收建立物件的引用。上面的例子就是向上轉型。有向上轉型自然也有向下轉型。向下轉型是指由於子類或實現類的引用不能直接指向父類或介面,需要強制轉換(子類或實現類)才能使用子類或實現類來接收父類或介面。例如:
package com.yx.yzh.test;
public class Test3<E, E2> extends Test1 {
public static void main(String[] args) {
//向上轉型
Test1 t = new Test3<Object,String>();
//向下轉型
Test3<Object,String> t2 = (Test3<Object,String>)t;
}

}

A、對於單字母類範型在java中約定:T表示型別(Type),E表示元素型別(Element)。
B、範型萬用字元?
泛型萬用字元?是指當有不明確型別的範型時,可以使用?來代替範型型別。需要注意的是,在建立類中需要明確指出某種型別,不能使用?範型萬用字元。因此?範型萬用字元只能使用在方法引數類型範型宣告中或者接收引用的那一方:

package com.yx.yzh.test;
public class Test5 extends Test1 {
public void helloWorld(){}
public void show(Test5<?> t){}
}

package com.yx.yzh.test;
public class Test6 extends Test5{
//不知道T宣告est6是什麼型別,但是建立Test6時必須明確給出型別
//Test6<?> t = new Test6();
//Test5<?> t2 = new Test6();
//方法中可以使用?範型萬用字元宣告,在呼叫時需要明確給出引用的具體範型型別
public void show(Test5<?> t){
t = this;
t.helloWorld();
}

public void helloWorld(){
	System.out.println("hello woold!");
}

public static void main(String[] args) {
	Test5<?> t = new Test6<String>();
	//t接受了具體範型型別
	t.show(t);
}

}

C、? extends E
? extends E表示該範型的型別的向上限定,也就是具體型別必須為E及其子類。同理,? extends E只能使用在方法引數類型範型宣告中或者接收引用的那一方。

已知Student繼承自People類
package com.yx.yzh.test;
public class People {}

package com.yx.yzh.test;
public class Student extends People{}

在聲明範型時,可以這樣理解? extends E中的?萬用字元:
package com.yx.yzh.test;
public class Test8 extends Test7 {
public static void main(String[] args) {
//final類不能被繼承,但? extends String這是個例外表達式
Test7<? extends String> t = new Test8();
Test7<? extends Object> t2 = new Test8();
//錯誤,因為?的父類為String,範型向上限定為String,所以?為Object會編譯錯誤
//Test7<? extends String> t3 = new Test8();
//Student是People的子類,而Test7與Test8範型一致,因此?表示Student,正確,
Test7<? extends People> t4 = new Test8();
}
//傳參時,傳入的引用的範型需要是People或People的子類或是實現類
public void show(Test7<? extends People> t1){
}
}

在? extends E中?也可以是可以是實現類與介面的關係:
現已知ClassT是InterfaceT的實現類
package com.yx.yzh.test;
public interface InterfaceT {}

package com.yx.yzh.test;
public class ClassT implements InterfaceT{}

那麼可以滿足? extends E表示式為:
Test7<? extends InterfaceT> t5 = new Test8();
需要注意的是,在方法中宣告型別時必須給出範型的具體型別,範型不能是E、T。例如,右邊表示式會編譯錯誤:Test7<? extends T> t5 = new Test8();但是在方法中可以宣告引數的範型為? extends E這種形式。
package com.yx.yzh.test;

public class Test8 extends Test7 {
public void show(Test7<? extends People> t1){}
//在呼叫時,需要給出T型別和?的具體型別,?就是相應的範型型別,且必須與T保持繼承關係
public void show2(Test7<? extends T> t1){}
}
但是,當方法中的範型範型為? extends T,在傳入引數時,前後必須明確給出範型型別,而不能使用? extends T進行宣告,否則在呼叫時會發生編譯錯誤:
package com.yx.yzh.test;

public class Test8 extends Test7 {
public static void main(String[] args) {
Test8 t6 = new Test8();//明確型別
t6.show2(t6);

}
public void show2(Test8<? extends T> t1){}

}
//可以編譯通過,但是呼叫方法時會報錯:
Test8<? extends People> t7 = new Test8();
T7.show2(t7);
應明確指出型別:
Test8 t6 = new Test8();//明確型別
t6.show2(t6);

D、? super E
?super E表示該範型的型別的向下限定,也就是具體型別必須為E及其父類。同理,?super E只能使用在方法引數類型範型宣告中(可以是父類或介面)或者接收引用的那一方(可以是父類或介面)。
package com.yx.yzh.test;
public class Test8 extends Test7 {
public static void main(String[] args) {
//儘量不要這麼宣告,需要宣告具體泛型型別
Test7<? extends People> t10 = new Test8();

	Test8<Student> t8 = new Test8<Student>();
	t8.show3(t8);//? super Student
	
	Test8<People> t9 = new Test8<People>();
	t9.show3(t9);//? super People
	
}	
public void show3(Test8<? super T> t1){
	
}

}

通過上面的理解,我們瞭解到,ArrayList(Collection<? extends E> c)傳入的引數必須是一個具體範型型別的實現類的引用。例如:
List listTest = new ArrayList();
List listTest2 = new ArrayList(listTest);
你會意識到,ArrayList(Collection<? extends E> c)其實就是一個拷貝現有集合的構造器。通過底層程式碼可以驗證這一點:
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}


List listTest = new ArrayList();
listTest.add(“hello world!”);
List listTest2 = new ArrayList(listTest);
System.out.println();
for(String s : listTest2){
System.out.print(s+" ");
}
/**output:
hello world!
*/