黑馬程式設計師——Java泛型知識點
一、Java沒有泛型之前
1.Java集合對元素型別沒有任何限制,這樣可能引發一些問題。例如,想建立一個儲存Dob物件的集合,但程式也可以輕易地將Cat物件“扔”進去,這樣可能會引發異常。
2.集合並不能儲存物件的狀態資訊,集合只知道它存放的是Object,因此取出集合元素後通常還需要進行強制型別轉換,這種強制型別轉換可能會引發ClassCastException異常。
如下面的程式:
上面的程式建立了一個List集合,只希望儲存Dog物件,卻也能將Cat物件“扔"進集合dogList中,於是程式試圖將一個Cat物件轉換成Dog型別,這將導致程式引發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{ }
二、引入泛型後
如下面的程式:
上面程式成功建立了一個只能儲存Dog物件的集合:dogList,不能儲存其它型別的物件。建立這種只能儲存特定型別物件的集合的方法就是泛型:在集合介面、類增加尖括號,尖括號裡放一個數據型別,即表明這個集合只能儲存這種型別的物件。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{ }
而上面的程式在取集合中的資料時,不需要強制型別轉換,因為集合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));
}
}