
一、泛型簡介
1.引入泛型的目的
了解引入泛型的動(dòng)機(jī),就先從語法糖開始了解。
語法糖
語法糖(Syntactic Sugar),也稱糖衣語法,是由英國計(jì)算機(jī)學(xué)家Peter.J.Landin發(fā)明的一個(gè)術(shù)語,指在計(jì)算機(jī)語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。Java中最常用的語法糖主要有泛型、變長參數(shù)、條件編譯、自動(dòng)拆裝箱、內(nèi)部類等。虛擬機(jī)并不支持這些語法,它們在編譯階段就被還原回了簡單的基礎(chǔ)語法結(jié)構(gòu),這個(gè)過程成為解語法糖。
泛型的目的: Java 泛型就是把一種語法糖,通過泛型使得在編譯階段完成一些類型轉(zhuǎn)換的工作,避免在運(yùn)行時(shí)強(qiáng)制類型轉(zhuǎn)換而出現(xiàn)ClassCastException,即類型轉(zhuǎn)換異常。
2.泛型初探
JDK 1.5 時(shí)才增加了泛型,并在很大程度上都是方便集合的使用,使其能夠記住其元素的數(shù)據(jù)類型。
在泛型(Generic type或Generics)出現(xiàn)之前,是這么寫代碼的:
public static void main(String[] args)
{
List list = new ArrayList();
list.add("123");
list.add("456");
System.out.println((String)list.get(0));
}
當(dāng)然這是完全允許的,因?yàn)長ist里面的內(nèi)容是Object類型的,自然任何對象類型都可以放入、都可以取出,但是這么寫會有兩個(gè)問題:
1、當(dāng)一個(gè)對象放入集合時(shí),集合不會記住此對象的類型,當(dāng)再次從集合中取出此對象時(shí),該對象的編譯類型變成了Object。
2、運(yùn)行時(shí)需要人為地強(qiáng)制轉(zhuǎn)換類型到具體目標(biāo),實(shí)際的程序絕不會這么簡單,一個(gè)不小心就會出現(xiàn)java.lang.ClassCastException。
所以,泛型出現(xiàn)之后,上面的代碼就改成了大家都熟知的寫法:
public static void main(String[] args)
{
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
System.out.println(list.get(0));
}
這就是泛型。泛型是對Java語言類型系統(tǒng)的一種擴(kuò)展,有點(diǎn)類似于C++的模板,可以把類型參數(shù)看作是使用參數(shù)化類型時(shí)指定的類型的一個(gè)占位符。引入泛型,是對Java語言一個(gè)較大的功能增強(qiáng),帶來了很多的好處。
3.泛型的好處
①類型安全。類型錯(cuò)誤現(xiàn)在在編譯期間就被捕獲到了,而不是在運(yùn)行時(shí)當(dāng)作java.lang.ClassCastException展示出來,將類型檢查從運(yùn)行時(shí)挪到編譯時(shí)有助于開發(fā)者更容易找到錯(cuò)誤,并提高程序的可靠性。
②消除了代碼中許多的強(qiáng)制類型轉(zhuǎn)換,增強(qiáng)了代碼的可讀性。
③為較大的優(yōu)化帶來了可能。
二、泛型的使用
1.泛型類和泛型接口
下面是JDK 1.5 以后,List接口,以及ArrayList類的代碼片段。
//定義接口時(shí)指定了一個(gè)類型形參,該形參名為E
public interface List<E> extends Collection<E> {
//在該接口里,E可以作為類型使用
public E get(int index) {}
public void add(E e) {}
}
//定義類時(shí)指定了一個(gè)類型形參,該形參名為E
public class ArrayList<E> extends AbstractList<E> implements List<E>{
//在該類里,E可以作為類型使用
public void set(E e) {
.......................
}
}
這就是泛型的實(shí)質(zhì):允許在定義接口、類時(shí)聲明類型形參,類型形參在整個(gè)接口、類體內(nèi)可當(dāng)成類型使用,幾乎所有可使用普通類型的地方都可以使用這種類型形參。
下面具體講解泛型類的使用。泛型接口的使用與泛型類幾乎相同,可以比對自行學(xué)習(xí)。
泛型類
定義一個(gè)容器類,存放鍵值對key-value,鍵值對的類型不確定,可以使用泛型來定義,分別指定為K和V
public class Container<K, V> {
private K key;
private V value;
public Container(K k, V v) {
key = k;
value = v;
}
public K getkey() {
return key;
}
public V getValue() {
return value;
}
public void setKey() {
this.key = key;
}
public void setValue() {
this.value = value;
}
}
在使用Container類時(shí),只需要指定K,V的具體類型即可,從而創(chuàng)建出邏輯上不同的Container實(shí)例,用來存放不同的數(shù)據(jù)類型。
public static void main(String[] args){
Container<String,String> c1=new Container<String ,String>("name","hello");
Container<String,Integer> c2=new Container<String,Integer>("age",22);
Container<Double,Double> c3=new Container<Double,Double>(1.1,1.3);
System.out.println(c1.getKey() + " : " + c1.getValue());
System.out.println(c2.getKey() + " : " + c2.getValue());
System.out.println(c3.getKey() + " : " + c3.getValue());
}
在JDK 1.7 增加了泛型的“菱形”語法:Java允許在構(gòu)造器后不需要帶完成的泛型信息,只要給出一對尖括號(<>)即可,Java可以推斷尖括號里應(yīng)該是什么泛型信息。
如下所示:
Container<String,String> c1=new Container<>("name","hello");
Container<String,Integer> c2=new Container<>("age",22);
泛型類派生子類
當(dāng)創(chuàng)建了帶泛型聲明的接口、父類之后,可以為該接口創(chuàng)建實(shí)現(xiàn)類,或者從該父類派生子類,需要注意:使用這些接口、父類派生子類時(shí)不能再包含類型形參,需要傳入具體的類型。
錯(cuò)誤的方式:
public class A extends Container<K, V>{}
正確的方式:
public class A extends Container<Integer, String>{}
也可以不指定具體的類型,如下:
public class A extends Container{}
此時(shí)系統(tǒng)會把K,V形參當(dāng)成Object類型處理。
2.泛型的方法
前面在介紹泛型類和泛型接口中提到,可以在泛型類、泛型接口的方法中,把泛型中聲明的類型形參當(dāng)成普通類型使用。 如下面的方式:
public class Container<K, V> {
........................
public K getkey() {
return key;
}
public void setKey() {
this.key = key;
}
....................
}
但在另外一些情況下,在類、接口中沒有使用泛型時(shí),定義方法時(shí)想定義類型形參,就會使用泛型方法。如下方式:
public class Main{
public static <T> void out(T t){
System.out.println(t);
}
public static void main(String[] args){
out("hansheng");
out(123);
}
}
所謂泛型方法,就是在聲明方法時(shí)定義一個(gè)或多個(gè)類型形參。泛型方法的用法格式如下:
修飾符<T, S> 返回值類型 方法名(形參列表)
{
方法體
}
注意:方法聲明中定義的形參只能在該方法里使用,而接口、類聲明中定義的類型形參則可以在整個(gè)接口、類中使用。
class Demo{
public <T> T fun(T t){ // 可以接收任意類型的數(shù)據(jù)
return t ; // 直接把參數(shù)返回
}
};
public class GenericsDemo26{
public static void main(String args[]){
Demo d = new Demo() ; // 實(shí)例化Demo對象
String str = d.fun("湯姆") ; // 傳遞字符串
int i = d.fun(30) ; // 傳遞數(shù)字,自動(dòng)裝箱
System.out.println(str) ; // 輸出內(nèi)容
System.out.println(i) ; // 輸出內(nèi)容
}
};
當(dāng)調(diào)用fun()方法時(shí),根據(jù)傳入的實(shí)際對象,編譯器就會判斷出類型形參T所代表的實(shí)際類型。
3.泛型構(gòu)造器
正如泛型方法允許在方法簽名中聲明類型形參一樣,Java也允許在構(gòu)造器簽名中聲明類型形參,這樣就產(chǎn)生了所謂的泛型構(gòu)造器。
和使用普通泛型方法一樣沒區(qū)別,一種是顯式指定泛型參數(shù),另一種是隱式推斷,如果是顯式指定則以顯式指定的類型參數(shù)為準(zhǔn),如果傳入的參數(shù)的類型和指定的類型實(shí)參不符,將會編譯報(bào)錯(cuò)。
public class Person {
public <T> Person(T t) {
System.out.println(t);
}
}
public static void main(String[] args){
//隱式
new Person(22);
//顯示
new<String>Person("hello");
}
這里唯一需要特殊注明的就是,如果構(gòu)造器是泛型構(gòu)造器,同時(shí)該類也是一個(gè)泛型類的情況下應(yīng)該如何使用泛型構(gòu)造器:
因?yàn)榉盒蜆?gòu)造器可以顯式指定自己的類型參數(shù)(需要用到菱形,放在構(gòu)造器之前),而泛型類自己的類型實(shí)參也需要指定(菱形放在構(gòu)造器之后),這就同時(shí)出現(xiàn)了兩個(gè)菱形了,這就會有一些小問題,具體用法再這里總結(jié)一下。
以下面這個(gè)例子為代表
public class Person<E> {
public <T> Person(T t) {
System.out.println(t);
}
}
這種用法:Person<String> a = new <Integer>Person<>(15); 這種語法不允許,會直接編譯報(bào)錯(cuò)!
三、類型通配符
顧名思義就是匹配任意類型的類型實(shí)參。
類型通配符是一個(gè)問號(?),將一個(gè)問號作為類型實(shí)參傳給List集合,寫作:List<?>(意思是元素類型未知的List)。這個(gè)問號(?)被成為通配符,它的元素類型可以匹配任何類型。
public void test(List<?> c){
for(int i =0;i<c.size();i++){
System.out.println(c.get(i));
}
}
現(xiàn)在可以傳入任何類型的List來調(diào)用test()方法,程序依然可以訪問集合c中的元素,其類型是Object。
List<?> c = new ArrayList<String>();
//編譯器報(bào)錯(cuò)
c.add(new Object());
但是并不能把元素加入到其中。因?yàn)槌绦驘o法確定c集合中元素的類型,所以不能向其添加對象。
下面就該引入帶限通配符,來確定集合元素中的類型。
帶限通配符
簡單來講,使用通配符的目的是來限制泛型的類型參數(shù)的類型,使其滿足某種條件,固定為某些類。
主要分為兩類即:上限通配符和下限通配符。
1.上限通配符
如果想限制使用泛型類別時(shí),只能用某個(gè)特定類型或者是其子類型才能實(shí)例化該類型時(shí),可以在定義類型時(shí),使用extends關(guān)鍵字指定這個(gè)類型必須是繼承某個(gè)類,或者實(shí)現(xiàn)某個(gè)接口,也可以是這個(gè)類或接口本身。
它表示集合中的所有元素都是Shape類型或者其子類
List<? extends Shape>
這就是所謂的上限通配符,使用關(guān)鍵字extends來實(shí)現(xiàn),實(shí)例化時(shí),指定類型實(shí)參只能是extends后類型的子類或其本身。
例如:
//Circle是其子類
List<? extends Shape> list = new ArrayList<Circle>();
這樣就確定集合中元素的類型,雖然不確定具體的類型,但最起碼知道其父類。然后進(jìn)行其他操作。
2.下限通配符
如果想限制使用泛型類別時(shí),只能用某個(gè)特定類型或者是其父類型才能實(shí)例化該類型時(shí),可以在定義類型時(shí),使用super關(guān)鍵字指定這個(gè)類型必須是是某個(gè)類的父類,或者是某個(gè)接口的父接口,也可以是這個(gè)類或接口本身。
它表示集合中的所有元素都是Circle類型或者其父類
List<? super Circle>
這就是所謂的下限通配符,使用關(guān)鍵字super來實(shí)現(xiàn),實(shí)例化時(shí),指定類型實(shí)參只能是extends后類型的子類或其本身。
例如:
//Shape是其父類
List<? super Circle> list = new ArrayList<Shape>();
四、類型擦除
Class c1=new ArrayList<Integer>().getClass();
Class c2=new ArrayList<String>().getClass();
System.out.println(c1==c2);
程序輸出:
true。
這是因?yàn)椴还転榉盒偷念愋托螀魅肽囊环N類型實(shí)參,對于Java來說,它們依然被當(dāng)成同一類處理,在內(nèi)存中也只占用一塊內(nèi)存空間。從Java泛型這一概念提出的目的來看,其只是作用于代碼編譯階段,在編譯過程中,對于正確檢驗(yàn)泛型結(jié)果后,會將泛型的相關(guān)信息擦出,也就是說,成功編譯過后的class文件中是不包含任何泛型信息的。泛型信息不會進(jìn)入到運(yùn)行時(shí)階段。
在靜態(tài)方法、靜態(tài)初始化塊或者靜態(tài)變量的聲明和初始化中不允許使用類型形參。由于系統(tǒng)中并不會真正生成泛型類,所以instanceof運(yùn)算符后不能使用泛型類。