1. 程式人生 > >黑馬程式設計師——Java泛型知識點

黑馬程式設計師——Java泛型知識點

一、Java沒有泛型之前

1.Java集合對元素型別沒有任何限制,這樣可能引發一些問題。例如,想建立一個儲存Dob物件的集合,但程式也可以輕易地將Cat物件“扔”進去,這樣可能會引發異常。

2.集合並不能儲存物件的狀態資訊,集合只知道它存放的是Object,因此取出集合元素後通常還需要進行強制型別轉換,這種強制型別轉換可能會引發ClassCastException異常。

如下面的程式:

public class ListError {
	
	public static void main(String[] args){
		List dogList = new ArrayList();
		dogList.add(new Dog());
		dogList.add(new Cat());
		
		for(int i=0; i<dogList.size(); i++){
			Dog dog = (Dog)dogList.get(i);
		}
	}

}
class Dog{
}
class Cat{
}
上面的程式建立了一個List集合,只希望儲存Dog物件,卻也能將Cat物件“扔"進集合dogList中,於是程式試圖將一個Cat物件轉換成Dog型別,這將導致程式引發ClassCastException異常。

二、引入泛型後

如下面的程式:

public class ListRight {
	
	public static void main(String[] args){
		List<Dog> dogList = new ArrayList<Dog>();
		dogList.add(new Dog());
//		dogList.add(new Cat());//這裡將引起編譯錯誤
		
		for(int i=0; i<dogList.size(); i++){
			Dog dog = (Dog)dogList.get(i);
		}
	}

}
class Dog{
}
class Cat{
}
上面程式成功建立了一個只能儲存Dog物件的集合:dogList,不能儲存其它型別的物件。建立這種只能儲存特定型別物件的集合的方法就是泛型:在集合介面、類增加尖括號,尖括號裡放一個數據型別,即表明這個集合只能儲存這種型別的物件。

而上面的程式在取集合中的資料時,不需要強制型別轉換,因為集合dogList物件能記住它儲存的物件都是Dog型別。這就是泛型的作用。

三、深入泛型

所謂泛型,就是允許在定義類、介面、方法時使用型別形參,這個型別形參將在宣告變數、建立物件、呼叫方法時動態地指定,即傳入實際的型別引數。

1.定義泛型介面、類

如下程式碼:

public interface InterfaceTest<E>{
	public void add(E x);
	public E get();
}
public class ClazzTest<T>{
	T info;
	public void setInfo(T info){
		this.info = info;
	}
	public T getInfo(){
		return this.info;
	}
}

2.從泛型類派生子類

當建立了帶泛型宣告的介面、父類之後,可以為該介面建立實現類,可以為該介面建立實現類,或者從該父類派生子類,但需要指出的是,當使用這些介面、父類時不能再包含型別形參。如下面的程式碼是錯誤的:

//定義類A繼承Apple類,Apple類不能跟型別形參
public class A extends Apple<T>{}

如果想從Apple類派生一個子類,則可以改為如下程式碼:
//使用Apple類時,為T形參傳入String型別
public class A extends Apple<String>{}

呼叫方法時必須為所有的資料形參傳入引數值,與呼叫方法不同的是,使用類、介面時可以不為型別形參傳入實際的型別引數,即如下程式碼也是正確的:
//使用Apple類時,沒有為T形參傳入實際的型別引數
public class A extends Apple{}

如果從Apple<String>類派生子類,則在Apple類中所有使用T型別形參的地方都將被替換成String型別,即它的子類將會繼承到String getInfo()和void setInfo(String info)兩個方法,如果子類需要重新父類的方法,就必須注意這點。下面程式示範了這一點:
public class A extends Apple<String>{
	
	public String getInfo(){
		return "子類的"+this.info;
	}
	//下面的方法是錯誤的,重寫父類方法是返回值型別不一致
//	public Object getInfo(){
//		return "子類的"+this.info;
//	}

}
class Apple<T>{
	T info;
	public T getInfo(){
		return info;
	}
	public void setInfo(T info){
		this.info = info;
	}
}

3.型別萬用字元

(1)使用型別萬用字元

前面講到,當使用一個泛型類時(包括宣告變數和建立物件),都應該為這個泛型類傳入一個型別實參。如果沒有傳入型別實參,編譯器就會提出泛型警告。假設現在需要定義一個方法,該方法裡有一個集合形參(如List集合),集合形參的元素型別是不確定的,那應該怎樣定義呢?

為了表示各種泛型List的父類,我們使用型別萬用字元,型別萬用字元是一個問號(?),講一個問號作為型別實參傳遞給List集合,寫作:List<?>(意思是未知型別元素的List)。型別萬用字元(?)可以匹配任何型別。

如下面程式:

	public void test(List<?> c){
		for(int i=0; i<c.size(); i++){
			System.out.println(c.get(i));
		}
	}

但這種帶萬用字元的List僅表示它是各種泛型List的父類,並不能把元素加入到其中。如下面程式碼將引起編譯錯誤。
List<?> c = new ArrayList<String>();
//下面程式引起編譯錯誤
c.add(new Object());

(2)設定型別萬用字元的上限

還有一種情形是,我們不想使這個List<?>是任何泛型List的父類,只想表示它是某一類泛型List的父類。也就是被限制的泛型萬用字元,表示如下:

//它表示所有Shape泛型List的父類
List<? extends Shape>

(3)設定型別萬用字元的下限

用法格式如下:

<? super Type>,這個萬用字元表示它必須是Type本身,或是Type的父類。

(4)設定型別形參的上限

Java泛型不僅允許在使用萬用字元形參時設定上限,而且可以在定義型別形參時設定上限,用於表示傳給該型別形參的實際型別要麼是該上限型別,要麼是該上限型別的子類。

下面程式示範了這種用法:

public class Apple<T extends Number>{
	T col;
	public static void main(String[] args){
		Apple<Integer> ai = new Apple<Integer>();
		Apple<Double> ad = new Apple<Double>();
		//下面程式碼引發編譯異常,下面程式碼試圖把String型別傳遞給T形參
		//但String不是Number的子型別
		Apple<String> as = new Apple<String>();
	}
}

四、泛型方法

泛型方法的用法格式如下:

修飾符  <T, S>  返回值型別  方法簽名(形參列表)

{

      //方法體...

}

與類、介面中使用泛型引數不同的是,方法中的泛型引數無需顯式傳入實際型別引數。

泛型方法與型別萬用字元的區別:

大多數時候都可以使用泛型方法來代替型別萬用字元。例如,對應Java的Collection介面中兩個方法定義:

public interface Collection<E>{
	boolean containsAll(Collection<?> c);
	boolean addAll(Collection<? extends E> c);
	...
}

上面集合中兩個方法的形參都採用了型別萬用字元的形式,也可以採用泛型方法的形式,如下所示:
public interface Collection<E>{
	<T> boolean containsAll(Collection<T> c);
	<T extends E> boolean addAll(Collection<T> c);
}

如果某個方法中的一個形參(a)的型別或返回值的型別依賴於另一個形參(b)的型別,則形參(b)的型別宣告不應該使用萬用字元——因為形參(a)或返回值的型別依賴於形參(b)的型別,如果形參(b)的型別無法確定,程式就無法定義形參(a)的型別。在這種情況下,只能考慮使用在方法簽名中宣告型別形參——也就是泛型方法。

五、擦除和轉換

當把一個具有泛型資訊的物件付給另一個沒有泛型資訊的變數時,所有在尖括號之間的型別資訊都將被扔掉。比如一個List<String>型別被轉換為List,則該List對集合元素的型別檢查變成了型別變數的上限,即Object。如果試圖重新轉換為List<String>,編譯可以通過,但取出元素時將引發異常。

下面程式示範了這種擦除:

public class ErasureTest{
	public static void main(String[] args){
		List<Integer> li = new ArrayList<Integer>();
		li.add(6);
		li.add(9);
		List list = li;
		//下面程式碼引起“未經檢查的轉換”警告,編譯、執行時完全正常
		List<String> ls = list;
		//只要訪問ls的元素,如下程式碼將引發執行時異常
		System.out.println(ls.get(0));
	}
}