一、前言
還記得JDK1.4時遍歷列表的辛酸嗎?我可是記憶猶新啊,那時因項目需求我從C#轉身到Java的懷抱,然后因JDK1.4少了泛型這樣語法糖(還有自動裝箱、拆箱),讓我受盡苦頭啊,不過也反映自己的水平還有待提高,呵呵。JDK1.5引入了泛型、自動裝箱拆箱等特性,C#到Java的過渡就流暢了不少。下面我們先重溫兩者非泛型和泛型的區別吧!
// 非泛型遍歷列表 List lst = new ArrayList(); lst.add(1); lst.add(3); int sum = 0; for (Iterator = lst.iterator(); lst.hasNext();){ Integer i = (Integer)lst.next(); sum += i.intValue(); } // 泛型遍歷列表 List<Integer> lst = new ArrayList<Integer>(); lst.add(1); lst.add(3); int sum = 0; for (Iterator = lst.iterator(); lst.hasNext();){ Integer i = lst.next(); sum += i; }
泛型的最主要作用是在編譯時期就檢查集合元素的類型,而不是運行時才拋出ClassCastException。
泛型的官方文檔:http://docs.oracle.com/javase/tutorial/java/generics/erasure.html
注意:以下內容基于JDK7和HotSpot。
二、認識泛型
在介紹之前先定義兩個測試類,分別是 類P 和 類S extends P 。
1. 聲明泛型變量,如 List<String> lst = new ArrayList<String>();
注意點——泛型不支持協變
// S為P的子類,但List<S>并不是List<P>的子類,也就是不支持協變 // 因此下列語句無法通過編譯 List<P> lst = new ArrayList<S>(); // 而數組支持協變 P[] array = new S[10];
注意點——父類作為類型參數,則可以子類實例作為集合元素
List<P> lst = new ArrayList<P>(); lst.add(new S());
2. 聲明帶通配符泛型變量,如 List<?> lst = new ArrayList<P>();
通配符 ? 表示類型參數為未知類型,因此可賦予任何類型的類型參數給它。
當集合的類型參數 ? 為時,無法向集合添加除null外的其他類型的實例。(null屬于所有類的子類,因此可以賦予到未知類型中)
List<?> lst = new ArrayList<P>(); lst = new ArrayList<S>(); // 以下這句將導致編譯失敗 lst.add(new S()); // 以下這句則OK lst.add(null);
因此帶通配符的泛型變量一般用于檢索遍歷集合元素使用,而不做添加元素的操作。
void read(List<?> lst){ for (Object o : lst){ System.out.println((o.toString()); } } List<String> lst = new ArrayList<String>(); lst.add("1"); lst.add("2"); read(lst);
到這里會發現使用帶通配符的泛型集合(unbounded wildcard generic type) 與 使用非泛型集合(raw type)的效果是一樣的,其實并不是這樣.
我們可以向非泛型集合添加任何類型的元素, 而通配符的泛型集合則只允許添加null而已, 從而提高了類型安全性. 而且我們還可以使用帶限制條件的帶邊界通配符的泛型集合呢!
3. 聲明帶邊界通配符 ? extends 的泛型變量,如 List<? extends P> lst = new ArrayList<S>();
邊界通配符 ? extends 限制了實際的類型參數必須為指定的類本身或其子類才能通過編譯。
void read(List<? extends P> lst){ for (P p : lst){ System.out.println(p); } } List<P> lst = new ArrayList<P>(); lst.add(new P()); lst.add(new S()); read(lst);
4. 聲明帶邊界通配符 ? super 的泛型變量,如 List<? super S> lst = new ArrayList<P>();
邊界通配符 ? super限制了實際的類型參數必須為指定的類本身或其父類才能通過編譯。
注意:集合元素的類型必須為指定的類本身或其子類。
void read(List<? super S> lst){ for (S s : lst) System.out.println(s); } List<P> lst = new ArrayList<P>(); lst.add(new S()); read(lst);
5. 定義泛型類或接口,如 class Fruit<T>{} 和 interface Fruit<T>{}
T為類型參數占位符,一般以單個大寫字母來命名。以下為推薦的占位符名稱:
K——鍵,比如映射的鍵。
V——值,比如List、Set的內容,Map中的值
E——異常類
T——泛型
除了異常類、枚舉和匿名內部類外,其他類或接口均可定義為泛型類。
泛型類的類型參數可供實例方法、實例字段和構造函數中使用,不能用于類方法、類字段和靜態代碼塊上。
class Fruit<T>{ // 類型參數占位符作為實例字段的類型 private T fruit; // 類型參數占位符作為實例方法的返回值類型 T getFruit(){ return fruit; } // 類型參數占位符作為實例方法的入參類型 void setFruit(T fruit){ this.fruit = fruit; } private List<T> fruits; // 類型參數占位符作為邊界通配符的限制條件 void setFruits(List<? extends T> lst){ fruits = (List<T>)lst; } // 類型參數占位符作為實例方法的入參類型的類型參數 void setFruits2(List<T> lst){ fruits = lst; } // 構造函數不用帶泛型 Fruit(){ // 類型參數占位符作為局部變量的類型 fruits = new ArrayList<T>(); T fruit = null; } }
和邊界通配符一般類型參數占位符也可帶邊界,如 class Fruit<T extends P>{} 。當有多個與關系的限制條件時,則用&來連接多個父類,如 class Fruit<T extends A&B&C&D>{} 。
也可以定義多個類型參數占位符,如 class Fruit<S,T>{} 、 class Fruit<S, T extends A>{} 等。
下面到關于繼承泛型類或接口的問題了,假設現在有泛型類P的類定義為 class P<T>{} ,那么在繼承類P時我們有兩種選擇
1. 指定類P的類型參數
2. 繼承類P的類型參數
// 1. 指定父類的類型參數 class S extends P<String>{} // 2. 繼承父類的類型參數 class S<T> extends P<T>{}
6.使用泛型類或接口,如 Fruit<?> fruit = new Fruit<Apple>();
現在問題來了,假如Fruit類定義如下: public class Fruit<T extends P>{}
那么假設使用方式為 Fruit<? extends String> fruit; ,大家決定編譯能通過嗎?答案是否定的,類型參數已經被限制為P或P的子類了,因此只有 Fruit<? extends P> 或 Fruit<? extends S> 可通過編譯。
7. 定義泛型方法
無論是實例方法、類方法還是抽象方法均可以定義為泛型方法。
// 實例方法 public <T> void say(T[] msgs){
for (T msg : msgs) System.out.println(msg.toString()); } public <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{ return clazz.newInstance(); } // 類方法 public static <T> void say(T msg){ System.out.println(msg.toString()); } public static <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{ return clazz.newInstance(); } // 抽象方法 public abstract <T> void say(T msg); public abstract <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{}
8. 使用泛型方法
使用泛型方法分別有 隱式指定實際類型 和 顯式指定實際類型 兩種形式。
P p = new P(); String msg = "Hello";
// 隱式指定實際類型 p.say(msg); // 顯式指定實際類型 p.<String>say(msg);
一般情況下使用隱式指定實際類型的方式即可。
9. 使用泛型數組
只能使用通配符來創建泛型數組
List<?>[] lsa = new ArrayList<String>[10]; // 拋異常 List<?>[] lsa = new ArrayList<?>[10]; List<String> list = new ArrayList<String>(); list.add("test"); lsa[0] = list; System.out.println(lsa[0].get(0));
四、類型擦除(Type Erasure)和代碼膨脹(Code Bloat)
到此大家對Java的泛型有了一定程度的了解了,但在應用時卻時不時就發生些匪夷所思的事情。在介紹這些詭異案例之前,我們要補補一些基礎知識,那就是Java到底是如何實現泛型的。
泛型的實現思路有兩種
1. Code Specialization:在實例化一個泛型類或泛型方法時將產生一份新的目標代碼(字節碼或二進制碼)。如針對一個泛型List,當程序中出現List<String>和List<Integer>時,則會生成List<String>,List<Integer>等的Class實例。
2. Code Sharing:對每個泛型只生成唯一一份目標代碼,該泛型類的所有實例的數據類型均映射到這份目標代碼中,在需要的時候執行類型檢查和類型轉換。如針對List<String>和List<Integer>只生成一個List<Object>的Class實例。
C++的模板 和 C# 就是典型的Code Specialization。由于在程序中出現N種L泛型List則會生成N個Class實例,因此會造成代碼膨脹(Code Bloat)。
而Java則采用Code Sharing的思路,并通過類型擦除(Type Erasure)來實現。
類型擦除的過程大致分為兩步:
①. 使用泛型參數extends的邊界類型來代替泛型參數(<T> 默認為<T extends Object>,<?>默認為<? extends Object>)。
②. 在需要的位置插入類型檢查和類型轉換的語句。
interface Comparable<T>{ int compareTo(T that); } final class NumericVal implements Comparable<NumericVal>{ public int compareTo(NumericVal that){ return 1;} }
擦除后:
interface Comparable{ int compareTo(Object that); } final class NumericVal implements Comparable{ public int compareTo(NumericVal that){ return 1;} // 編譯器自動生成 public int compareTo(Object that){ return this.compareTo((NumbericVal)that); }
}
也就是說
List<String> lstStr = new ArrayList<String>(); List<Integer> intStr = new ArrayList<Integer>(); System.out.println(lstStr.getClass() == intStr.getClas()); // 顯示true,因為lstStr和intStr的類型均被擦除為List了
五、各種基于Type Erasure的泛型的詭異場景
1. 泛型類型共享類變量
class Fruit<T>{ static String price = 0; } Fruit<Apple>.price = 12; Fruit<Pear>.price = 5; System.out.println(Fruit.<Apple>.price); // 輸出5
2. instanceof 類型參數占位符 拋出編譯異常
List<String> strLst = new ArrayList<String>(); if (strLst instanceof List<String>){} // 不通過編譯 if (strLst instanceof List){} // 通過編譯
3. new 類型參數占位符 拋出編譯異常
class P<T>{ T val = new T(); // 不通過編譯 }
4. 定義泛型異常類 拋出編譯異常
class MyException<T> extends Exception{} // 不通過編譯
5. 不同的泛型類型形參無法作為不同描述符標識來區分方法
// 視為相同的方法,因此會出現沖突 public void say(List<String> msg){} public void say(List<Integer> number){} // JDK6后可通過不同的返回值類來解決沖突 // 對于Java語言而言,方法的簽名僅為方法名+參數列表,但對于Bytecodes而言方法的簽名還包含返回值類型。因此在這種特殊情況下,Java編譯器允許這種處理手段 public void say(List<String> msg){} public int say(List<Integer> number){}
六、再深入一些
1. 采用隱式指定類型參數類型的方式調用泛型方法,那到底是如何決定的實際類型呢?
假如現有一個泛型方法的定義為 <T extends Number> T handle(T arg1, T arg2){ return arg1;}
那么根據類型擦除的操作步驟,T的實際類型必須是Number的。看看字節碼吧 Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;Ljava/lang/Number;
剩下的就是類型檢查和類型轉換的活了,根據不同的入參類型和對返回值進行類型轉換的組合將導致不同的結果。
// 編譯時報“交叉類型”編譯失敗 Integer ret = handle(1, 1L); // 編譯成功 Number ret = handle(1, 1L); Integer ret = handle(1,1);
Number ret = handle(1, 1L)對應的Bytecodes為
14: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 17: invokevirtual #5 // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;
而Interger ret = handle(1, 1L)對應的Bytescodes則多了checkcast指令用于作類型轉換
14: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 17: invokevirtual #5 // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number; 20: checkcast #6 // class java/lang/Integer
根據上述規則,所以下列代碼會由于方法定義沖突而編譯失敗
// 編譯失敗 <T extends String> void println(T msg){} void println(String msg){}
2. 效果一致但寫法不同的兩個泛型方法
public static <T extends P> T getP1(Class<T> clazz){ T ret = null; try{ ret = clazz.newInstance(); } catch(InstantiationException|IllegalAccessException e){} return ret; }
} public static <T> T getP2(Class<? extends P> clazz){ T ret = null; try{ ret = (T)clazz.newInstance(); } catch(InstantiationException|IllegalAccessException e){} return ret; }
}
getP1的內容不難理解,類型參數占位符T會被編譯成P,因此類型擦除后的代碼為:
public static P getP1(Class clazz){ P ret = null; try{ ret = (P)clazz.newInstance(); } catch(InstantiationException|IllegalAccessException e){} return ret; } }
而getP2中T被編譯為Object,而clazz.newInstance()返回值類型為Object,那么為什么要加(T)來進行顯式的類型轉換呢?但假如將<T>改成<T extends Number>,那顯式類型轉換就變為必須品了。我猜想是因為getP2的書寫方式導致返回值與入參的兩者的類型參數是沒有任何關聯的,無法保證一定能成功地執行隱式類型轉換,因此規定開發人員必須進行顯式的類型轉換,否則就無法通過編譯。但最吊的是Bytecodes里沒有類型轉換的語句
3: invokevirtual #2 // Method java/lang/Class.newInstance:()Ljava/lang/Object; 6: astore_1
七、總結
若有紕漏請大家指正,謝謝!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4288614.html ^_^肥仔John
八、參考
http://blog.zhaojie.me/2010/02/why-not-csharp-on-jvm-type-erasure.html
http://blog.csdn.net/lonelyroamer/article/details/7868820
http://www.programcreek.com/2013/12/raw-type-set-vs-unbounded-wildcard-set/
文章列表