Java 泛型(1):基本原理
1.1????? 擦除
15.7例子
Java的泛型是同伙擦除來實現的,在泛型代碼內部,無法獲得任何有關泛型參數類型的信息(這一點有別于C++等實現),這意味著你在使用泛型時,無法知道任何類型信息,只知道你在使用一個對象,因此List<String>和List<Integer>在運行時事實上是相同的類型。
1.1.1??????? C++的實現
15.7.1例子
C++的實現與Java實現不同,當你實例化模板時,C++編譯器會進行檢查,模板代碼知道參數類型,所以如果調用了類型的某些方法,將由編譯器在編譯時進行檢查
?
1.1.2??????? 為何采用擦除
泛型不是Java出現時就有的組成部分,而是中途加入的,為了向下兼容許多現有的代碼和類文件,并且保持其原有的含義,所以采用了擦除這一解決方案,允許非泛型和泛型代碼共存。
?????? 必須意識到,擦除并不是實現泛型最好的技術,如果泛型出現在Java 1.0,那么將不會使用擦除來實現,而是使用具體化,這樣你就可以在類型參數上執行基于類型的操作和反射,擦除減少了泛型的泛化性,使得Java的泛型沒有本來設想的那么有用,但是泛型在Java中仍然是一個很有用的特性。
1.1.3??????? 擦除引入的問題
由于擦除的原因,Java的泛型不能用于顯示地引用運行時類型的操作,例如轉型、instanceof、new等操作。因為所有的參數類型都丟失了,在編寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有了參數類型信息,例如:
Foo<Cat> f = new Foo<Cat>();
我們通常會認為,Foo的代碼應該知道操作的對象是Cat,而且從語法上也給人這樣的暗示:在Foo類中,所有的地方類型T都被替換成了Cat。但是事實卻完全不是這樣,你必須要知道,它只是一個Object。
?
?
1.2????? 邊界
邊界就是在泛型的參數類型上設置限制條件,比如<T extends Shape>等,使用邊界讓你可以強制規定泛型可以應用的類型。
??? 因為擦除了類型信息,導致我們只能調用那些Object的方法,但是,我們可以通過將這個參數限制為某個類型的子集,那么你就可以用這些類型子集的方法。我們可以通過繼承來消除邊界書寫上的冗余,如下例所示,通過繼承,在每個層次上添加邊界限制,在不斷的繼承中,T擁有了越來越多的成員。
public interface IGetName {public String getName();}public interface Weight {public int getWeight();}public class Shape {int x;int y;int z;}public class Cat<T> {T item;Cat(T item) {this.item = item;}}public class MyCat<T extends IGetName> extends Cat<T>{MyCat(T item) {super(item);}public String getMyCatName() {return item.getName();}}public class MyCats<T extends Shape & IGetName & Weight> extends MyCat<T> {MyCats(T item) {super(item);// TODO Auto-generated constructor stub }int getCatWeight() {return item.getWeight();}int getShape() {return item.x + item.y + item.z;}}?
?
注意:在繼承中,T的作用域必須越來越窄,不能越來越寬,例如:IgetName接口在MyCat的類中已經作了限制,如果在MyCats的繼承中將IGetName去掉(class MyCats<T extends Shape & Weight> extends MyCat<T>),將會出現The type T is not a valid substitute for the bounded parameter <T extends IGetName> of the type MyCat<T>的錯誤,原因就是T的作用域變寬了(父類的T必須是繼承IGetName,而子類沒有這一限制)。
??? 在類型邊界限制中,不管是類還是接口都是使用extends,中間使用&分隔,同時需要注意的時,如果有類繼承,那么類必須寫在第一個,接口寫在第二個到第n個,<T extends 類 & 接口1 & 接口2 & 接口3 ...>。
1.3????? 泛型數組
1.3.1??????? 為什么Java不能創建泛型數組
擦除會移除參數類型信息,而數組必須知道他們所持有的確切類型,以強制保障類型安全
在Java中,Object[]數組可以是任何數組的父類,或者說,任何一個數組都可以向上轉型成它在定義時指定元素類型的父類的數組,這個時候如果我們往里面放不同于原始數據類型 但是滿足后來使用的父類類型的話,編譯不會有問題,但是在運行時會檢查加入數組的對象的類型,于是會拋ArrayStoreException:
String[]strArray=newString[20];Object[]objArray=strArray;objArray[0]=newInteger(1);// throws ArrayStoreException at runtime?
因為Java的范型會在編譯后將類型信息抹掉,這樣如果Java允許我們使用類似Map<Integer,String>[]mapArray=newMap<Integer,String>[20];這樣的語句的話,我們在隨后的代碼中可以把它轉型為Object[]然后往里面放Map<Double, String>實例。這樣做不但編譯器不能發現類型錯誤,就連運行時的數組存儲檢查對它也無能為力,它能看到的是我們往里面放Map的對象,我們定義的<Integer, String>在這個時候已經被抹掉了,于是而對它而言,只要是Map,都是合法的。想想看,我們本來定義的是裝Map<Integer, String>的數組,結果我們卻可以往里面放任何Map,接下來如果有代碼試圖按原有的定義去取值,后果是什么不言自明。
?????? 如上原因,Java中不能創建泛型數組,一般的解決方案是在你想創建泛型數組的地方使用ArrayList。
1.3.2??????? 生成泛型數組的折中方法
?????? 有趣的是,雖然Java中不能創建泛型數組,但是你卻可以定義一個泛型數組的引用,而且編譯器不會產生任何警告,但是你卻永遠不能創建這個數組。
Foo<Cat> []f = new Foo<Cat>[SIZE]; //error Foo<Cat> [f]; //right?????? 既然無法創建泛型數組,那么泛型數組的引用拿來干什么呢,考慮以下用法:
????????????? Foo<Cat> []f = (Foo<Cat>)new Object[SIZE];? //compile ok, run error
數組的每個元素應該持有相同的類型,那么通過創建一個Object的數組進行強轉成泛型數組是否可行呢?事實上以上代碼是可以編譯的,但是當你運行的時候,卻會拋出ClassCastException異常。原因是雖然我們將Object進行了強轉(所以編譯期不會報錯),但是在運行時它仍然是Object,所以引發了類異常。
?
事實上,生成泛型數組的唯一方式是創建一個被擦除類型的新數組,然后對其轉型,例如: T [] array;? array = (T[])new Object[SIZE]; 考慮以下例子,我們使用泛型數組來生成一個自己的簡單的數組:
class MyArray<T> {private T[] array;public MyArray(int size) {array = (T[])new Object[size];}public void set(T item, int n) {array[n] = item;}public T get(int n) {return array[n];}public T[] getArray( ) {return array;}public static void prt(String str) {System.out.println(str);}public static void main(String []args) {MyArray<String> my = new MyArray(3);my.set("a", 0);my.set("b", 1);my.set("c", 2);prt(my.get(1));//String[] o = my.getArray(); //run error Object[] o = my.getArray();prt(Arrays.toString(o));}}//out b[a, b, c]?
?
注意:我們仍然不能使用T[] array = new T[SIZE];只能使用創建對象數組,然后強轉的方式,因為Object是任何類型的基類,所以我們可以將String類型的對象賦值到數組上,注意方法getArray(),它的返回值為T[],在main函數中,聲明的是<String>,所以我們嘗試將其賦予到String[]數組上,但是此時運行時發生了類型錯誤,這是因為本質上我們的數組還是Object[],所以在運行中試圖將一個Object[]賦值到String[],自然會發生異常。
?????? 事實上,更加穩妥的做法是將上述代碼中的數組聲明,更改成Object[] array,由于Java的擦除技術,所以我們運行的時候類型都會是Object,如果我們立刻將其轉型成T[],那么我們編譯期就會丟失數組的實際類型(Object),此時編譯器可能錯過一些潛在的錯誤檢查。另外一點,使用Object[]聲明可以隨時提醒我們,這個數組運行時是Object,而不是T[]。
?
1.3.3??????? 生成“真正的”泛型數組
仍然考慮上述的例子,如果我們將參數類型傳入進去,那么我們可以生成一個“真正的”泛型數組,通過反射技術,生成了實際傳入的參數類型(String)的數組,這樣array的實際類型就是String[],而不是上例的Object[],所以在main()中getArray()直接返回給String[]類型的引用沒有任何問題。
class MyArray<T> {private T[] array;public MyArray(Class<T> type, int size) {array = (T[])Array.newInstance(type, size);}public void set(T item, int n) {array[n] = item;}public T[] getArray( ) {return array;}public static void main(String []args) {MyArray<String> my = new MyArray(String.class, 3);my.set("a", 0);my.set("b", 1);my.set("c", 2);String[] o = my.getArray();System.out.println(Arrays.toString(o));}}?
?
轉載于:https://www.cnblogs.com/LemonPi/p/11040252.html
總結
以上是生活随笔為你收集整理的Java 泛型(1):基本原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TypeError: missing 1
- 下一篇: C# 延迟初始化 LazyT