泛型是Java 1.5引入的新特性。泛型的本質是參數化類型,這種參數類型可以用在類、變量、接口和方法的創建中,分別稱為泛型類、泛型變量、泛型接口、泛型方法。將集合聲明參數化以及使用JDK提供的泛型和泛型方法是相對簡單的,而編寫自己的泛型類型會比較困難,但是還是值得思考與學習如何去編寫。
1、泛型的優勢
提高代碼的安全性和表述性
在沒有泛型的情況的下,通過對類型Object
的引用來實現參數的“任意化”,缺點是要做顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以預知的情況下進行的。一個錯誤的示范如下:
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("String");
int isInt = (int) list.get(1); //ClassCastException
}
本例中對于強制類型轉換錯誤的情況,編譯器在編譯時并不提示錯誤,在運行的時候才出現ClassCastException
異常,這樣便存在著安全隱患。(Effective Java第23條:請不要在新代碼中使用原生態類型)
提高代碼的重用率
利用泛型類可以選擇具體的類型對類進行復用相對比較容易理解,具體的說明如下:
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
這樣我們的Box類便可以得到復用,我們可以將T替換成任何我們想要的類型:
Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
2、泛型的使用
泛型類
泛型類中使用通配泛型T相比較用Object
類型強制轉換的優勢已經介紹過,詳見章節1中Box
類中泛型的使用。
泛型方法
泛型類在多個方法簽名間實施類型約束。在 List<V>
中,類型參數 V
出現在 get()
、add()
、contains()
等方法的簽名中。當創建一個 Map<K, V>
類型的變量時,您就在方法之間宣稱一個類型約束。您傳遞給 add()
的值將與 get()
返回的值的類型相同。
類似地,之所以聲明泛型方法,一般是因為您想要在該方法的多個參數之間宣稱一個類型約束。舉例如下:
public static void main(String[] args) throws ClassNotFoundException {
String str=get("Hello", "World");
System.out.println(str);
}
public static <T, U> T get(T t, U u) {
if (u != null)
return t;
else
return null;
}
泛型變量
在泛型類、泛型方法的介紹中,我們已經使用到了泛型變量,申明泛型變量主要是因為我們在定義泛型變量的時候,我們并不知道這個泛型類型T,到底是什么類型,所以,只能默認T為原始類型Object,而是使用時確定泛型T的具體類型,也是用來做類型限定的。
通配符
通配泛型的使用相對基本的泛型類型的使用而言具有一定的難度,不過通配符可以提高API的靈活性。舉例如下定義3個類:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
通過通配泛型,可以定義出受檢的泛型類型,也能夠將幾個類的關系體現出來。
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
3、數組與泛型
數組與泛型相比,有兩個重要的不同點。首先,數組是協變的(covariant
)。這就是說如果sub
是super
的子類型,那么數組類型sub[]
就是super[]
的子類型。然而,泛型是不可變的(invariant
),對于任意兩個不同的類型type1
和type2
,List<type1>
既不是List<type1>
的子類型,也不是List<type2>
的超類型。
數組和泛型的第二大區別在于數組是具體化的,因此數組會在運行時才知道并檢查他們的元素類型約束。相比之下,泛型是通過擦除來實現的,因此泛型只在編譯時強化他們的類型信息,并在運行時丟棄他們的元素類型信息。
Object[] arr = new Long[1];
arr[0] = "I don't fit in"; //運行失敗,拋出ArrayStoreException
List<Object> list = new ArrayList<>(); //編譯不通過,類型不匹配
list.add(I don't fit in);
(Effective Java第25條:列表優先于數組)
由于以上這些根本的區別,數組和泛型不能很好的混合使用,例如:創建泛型或者類型參數的數組是非法的。
4、類型擦除
不同的語言在實現泛型時采用的方式不同,C++的模板會在編譯時根據參數類型的不同生成不同的代碼,而Java的泛型是一種偽泛型,編譯為字節碼時參數類型會在代碼中被擦除,單獨記錄在Class文件的attributes
域,而在使用泛型處做類型檢查與類型轉換。
TIPS: 區別Java語言的編譯時和運行時是非常重要的,泛型只在編譯時強化他們的類型信息,并在運行時丟棄他們的元素類型信息。泛型的運行時擦除可以通過Java提供的反射機制進行證明,比如通過反射調用List<String>
容器的add()
方法,繞過泛型檢查,成功插入Integer
類型的變量。
假設參數類型的占位符為T,擦除規則如下:
-
<T>
擦除后變為Obecjt
-
<? extends A>
擦除后變為A
*<? super A>
擦除后變為Object
上述擦除規則叫做保留上界。泛型擦除之后保留原始類型。原始類型raw type
就是擦除去了泛型信息,最后在字節碼中的類型變量的真正類型。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除crased
,并使用其限定類型(無限定的變量用Object
)替換。
但是要區分原始類型和泛型變量的類型
在調用泛型方法的時候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情況下,泛型變量的類型為 該方法中的幾種類型的同一個父類的最小級,直到Object。
在指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。
public class Test2{
public static void main(String[] args) {
/**不指定泛型的時候*/
int i=Test2.add(1, 2); //兩參數都是Integer,所以T為Integer類型
Number f=Test2.add(1 , 1.2);//參數是Integer和Float,取同一父類的最小級Number
Object o=Test2.add(1, "asd"); //參數是Integer和String,取同一父類的最小級Object
/**指定泛型的時候*/
int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能為Integer類型或者其子類
int b=Test2.<Integer>add(1 , 2.2);//編譯錯誤,指定了Integer,不能為Float
Number c=Test2.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
5、類型擦除的問題和解決方法
Java的泛型是偽泛型。為什么說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型信息都會被擦除掉。正確理解泛型概念的首要前提是理解類型擦出(type erasure
)。Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱為類型擦除。
因為種種原因,Java不能實現真正的泛型,只能使用類型擦除來實現偽泛型,這樣雖然不會有類型膨脹的問題,但是也引起了許多新的問題。所以,Sun對這些問題作出了許多限制,避免我們犯各種錯誤。
1、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
2、自動類型轉換
因為類型擦除的問題,所以所有的泛型類型變量最后都會被替換為原始類型。這樣就引起了一個問題,既然都被替換為原始類型,那么為什么我們在獲取的時候,不需要進行強制類型轉換呢?實際上,使用泛型的容器會在return
之前,會根據泛型變量進行強轉。
3、類型擦除與多態的沖突和解決方法
子類實現父類中的泛型的方法時注意因為擦除而引起的語義的變化
4、泛型類型變量不能是基本數據類型
不能用類型參數替換基本類型。就比如,沒有ArrayList<double>
,只有ArrayList<Double>
。因為當類型擦除后,ArrayList
的原始類型變為Object
,但是Object
類型不能存儲double
值,只能引用Double
的值。
5、運行時類型查詢
由于運行時類型已經擦除,所以進行泛型類型的查詢是不正確的,對泛型的類型查詢Java限定了這種類型查詢的方式if( arrayList instanceof ArrayList<?>)
6、異常中使用泛型的問題
不能拋出也不能捕獲泛型類的對象。因為異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉,類型信息被擦除后,那么很有可能兩個地方的catch
都變為原始類型Object
,這個當然就是不行的。就好比,catch
兩個一模一樣的普通異常,不能通過編譯一樣。
根據異常捕獲的原則,一定是子類在前面,父類在后面,那么上面就違背了這個原則。即使你在使用該靜態方法的使用T是ArrayIndexOutofBounds
,在編譯之后還是會變成Throwable
,ArrayIndexOutofBounds
是IndexOutofBounds
的子類,違背了異常捕獲的原則。所以Java為了避免這樣的情況,禁止在catch
子句中使用泛型變量。
7、泛型類型的實例化
不能實例化泛型類型
8、類型擦除后的沖突
當泛型類型被擦除后,創建條件不能產生沖突,如下代碼段中泛型擦除后方法
boolean equals(T)
變成了方法boolean equals(Object)
這與Object.equals
方法是沖突的!當然,補救的辦法是重新命名引發錯誤的方法。
class Pair<T> {
public boolean equals(T value) {
return null;
}
}
9、泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數。因為泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。但是要注意區分一種情況,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T,是沒有錯誤的。
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
public static <T>T show(T one){//這是正確的
return null;
}
}
參考資料:
[1]:《Effective Java》
[2]:關于Java泛型深入理解小總結
[3]:Java泛型詳解
[4]:Java泛型的實現:原理與問題
[5]:Java中的逆變與協變
[6]:java泛型(一)、泛型的基本介紹和使用
[7]:java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題