泛型是Java中一項十分重要的特性,在Java 5版本被引入,在日常的編程過程中,有很多依賴泛型的場景,尤其是在集合容器類的使用過程中,更是離不開泛型的影子。
泛型的作用
泛型提供的功能有:參數(shù)化類型,以及編譯期類型檢查。
1 參數(shù)化類型
在方法的定義中,方法的參數(shù)稱為形參,在實際調(diào)用方法時傳遞實參。泛型的使用中,可以將類型定義為一個參數(shù),在實際使用時再傳遞具體類型。將泛型這種使用方式稱之為參數(shù)化類型。
在集合類的使用中,若不使用泛型,則需要對每一種元素類型設計相同的集合操作,例如:
class ListInteger{
//...
}
class ListDouble{
//...
}
通過泛型的使用,可以避免這種重復定義的現(xiàn)象,定義一套集合操作,來應對所有元素類型,例如:
class List<E>{
//...
}
在使用中傳遞不同的元素類型給List即可。
這里使用的字符
E并無特殊含義,只是為了便于理解而已。泛型中通常使用的字符及表示意義為:
K:鍵值對中的key
V:鍵值對中的value
E:集合中的element
T:類的類型type
2 編譯期類型檢查
對于集合ArrayList而言,若不指定具體元素類型,則使用過程中可能出現(xiàn)以下情況:
List list = new ArrayList();
list.add("abc");
list.add(123);
for (Object obj : list) {
String e = (String) obj;//ClassCastException
}
這段代碼在編譯期沒問題,運行時會報出java.lang.ClassCastException。
這種對集合的使用方式存在兩個問題:一是add添加元素時,因為元素聲明為Object類型,任意類型元素都可以添加到集合中,所以在添加元素時需要使用者自己注意選擇的元素類型;二是get取元素時需要強制類型轉(zhuǎn)換,需要開發(fā)人員記住操作的元素類型,否則可能拋出ClassCastException異常。
在聲明集合時指定元素類型則可以避免以上兩種問題:
List<String> list = new ArrayList<String>();
list.add("abc");
//list.add(123); compile error
for (String obj : list) {
String e = obj;
}
通過泛型的使用,指定集合元素的類型,則可以在編譯期就進行元素類型檢查,并且get獲取元素時無需進行強制類型轉(zhuǎn)換。
這里稱獲取元素無需進行強制類型轉(zhuǎn)換,其實并不準確,嚴格來講,使用泛型在進行獲取元素操作時,進行的是隱式類型轉(zhuǎn)換,所以仍然存在強制類型轉(zhuǎn)換的操作。
ArrayList中的隱式類型轉(zhuǎn)換:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
泛型的使用
泛型可以應用于定義泛型類、泛型接口和泛型方法。
1 泛型類
泛型類的定義方式較為簡單,通過將類型抽象為參數(shù),附加在類名稱后,即可完成泛型類的定義,示例:
public class Test {
public static void main(String[] args) {
User<Integer> user = new User<>();
user.setAttribute(123);
// user.setAttribute("abc");compile error
Integer attribute = user.getAttribute();
}
}
class User<T> {
private T attribute;
public User() {
}
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
}
通過使用泛型類,可以在編譯期進行參數(shù)類型檢查,并且使用時無需進行強制類型轉(zhuǎn)換。
2 泛型接口
泛型接口的使用與泛型類較為相似,在接口名稱后添加表示類型的字符即可,示例:
interface Person<T> {
T getAttribute();
void setAttribute(T attribute);
}
3 泛型方法
在前面的泛型類中定義的如下方法:
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
雖然使用了參數(shù)化類型,但是并不算是泛型方法,因為這些方法中使用的參數(shù)類型是泛型類定義的。泛型方法中定義了自己使用的類型,示例:
public <T> void genericsMethod(T parameter){
//...
}
泛型與繼承
在泛型的使用中,關于繼承方面需要注意,示例:
public class Test {
public static void main(String[] args) {
A<Number> aNumber = new A<>();
A<Integer> aInteger = new A<>();
// aNumber = aInteger; compile error
System.out.println(aNumber.getClass() == aInteger.getClass()); // true
}
static class A<T>{}
}
雖然Integer是Number的子類型,但是A<Integer>并不是A<Number>的子類型。
事實上,編譯器會在編譯階段進行類型檢查后,會擦除泛型的類型信息,也就是說在運行期
A<Integer>和A<Number>是同一個類。
對于泛型容器類List<E>,在進行泛型擦除后,記錄的元素類型為其聲明的最左邊父類型,此處即為Object類型,示例:
public class Test {
public static void main(String[] args) throws Exception {
List<Integer> integers = new ArrayList<>();
integers.getClass().getDeclaredMethod("add", Object.class).invoke(integers, "abc");
}
}
代碼在編譯期和運行期都沒問題,在編譯生成的.class文件中,Integer元素類型被擦除后,容器的元素類型記錄為Object類型。
泛型使用中的繼承定義方式如下:
public class Test {
public static void main(String[] args) {
A<Integer> a = new A<>();
B<Integer> b = new B<>();
a = b;
}
}
class A<T>{}
class B<T> extends A<T>{}
在繼承關系中使用同一個參數(shù)類型,以此實現(xiàn)泛型類的繼承。在JDK中ArrayList<E>、List<E>與Collection<E>采用的就是這種方式。
但是這種繼承方式依然不能滿足前面提到的使用場景,例如如下使用List方式:
public class Test {
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// numberList = integerList; compile error
}
}
雖然Integer是Number的子類型,但List<Integer>卻不是List<Number>的子類型,問題與前面的示例中相同。
通配符
通配符號?是一種實參類型,表示類型不確定的意思,或者表示任意一種類型,選擇?作為類型的目的是為了匹配更大范圍的類型,所以這里?是一種具體的類型。
這里稱
?類型不確定,又稱?是一種具體的類型,這種說法是相對于前面的類型參數(shù)T而言的,T表示類型形參,使用時被替代為傳入的具體類型,而?就是一種具體類型,不會被別的具體類型替代。
在前面有關泛型的繼承關系中,遇到List<Integer>不是List<Number>的子類型問題,可以使用通配符號?表示具體類型,這樣則可以匹配任意的參數(shù)類型,示例:
public class Test {
public static void main(String[] args) {
List<?> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList;
}
}
既然?可以表示所有類型,當然也可以表示Integer類型,所以代碼可以編譯通過。
在平常的使用中,類型的選擇范圍并非如此隨意,更多時候在定義泛型類、接口或方法時,限定了能夠使用的類型范圍。
1 限定上界
使用extends關鍵字限定參數(shù)類型能夠選擇的上界,示例:
public class Test {
public static void main(String[] args) {
GenericsClass<Integer> integerObj = new GenericsClass<>();
// GenericsClass<String> stringObj = new GenericsClass<>(); compile error
Test.genericsMethod1(new ArrayList<Integer>());
// Test.genericsMethod1(new ArrayList<String>()); compile error
Test.genericsMethod2(new ArrayList<Integer>());
// Test.genericsMethod2(new ArrayList<String>()); compile error
}
static class GenericsClass<T extends Number>{
//...
}
static <T extends Number> void genericsMethod1(List<T> list) {
// list.add(1); compile error
}
static void genericsMethod2(List<? extends Number> list) {
// list.add(1); compile error
}
}
GenericsClass類中通過<T extends Number>限定參數(shù)類型為Number的子類型,genericsMethod1、genericsMethod2同樣使用extends關鍵字限定類型上界。
genericsMethod1與genericsMethod2分別使用了T和?作為參數(shù)類型符號,在限定類型范圍上,兩者作用相同。不同之外在于,使用T表示類型形參,在genericsMethod1方法體內(nèi)可以引用T類型相關的操作,但是?則無法引用。
這里需要注意一點,若使用具有上界的泛型來作為集合的元素類型時,因為此時無法確定集合的元素類型,所以無法向集合中添加元素,示例:
static <T extends Number> void genericsMethod1(List<T> list) {
// list.add(1); compile error
}
static void genericsMethod2(List<? extends Number> list) {
// list.add(1); compile error
}
2 限定下界
使用super關鍵字限定參數(shù)類型能夠選擇的下界,示例:
public class Test {
public static void main(String[] args) {
Test.genericsMethod2(new ArrayList<Integer>());
// Test.genericsMethod2(new ArrayList<String>()); compile error
}
// static class GenericsClass<? super Integer>{ compile error
// //...
// }
// static <T super Integer> void genericsMethod1(List<T> list) { compile error
// //...
// }
static void genericsMethod2(List<? super Integer> list) {
list.add(1);
}
}
由示例可知,<? super Integer>的形式限定元素的下界為Integer類型,則此時可以對集合進行添加Integer元素操作。
由示例同樣可知,使用super關鍵字限定參數(shù)類型下界,與使用extends關鍵字限定參數(shù)類型的上界有所不同,最大的區(qū)別就是:類型形參T不能與super關鍵字配合使用。若可以配合使用,則會存在以下問題:
<T extends Integer>表示T類為Integer的子類型,則T類型屬性可以訪問Integer類型中的部分屬性;<T super Integer>的描述表示T類為Integer的父類,則T類型屬性不確定其父類為何類,也可能為Serializable,那么此時將不具備任何屬性,因為不確定,所以無法進行操作;<T extends Integer>在編譯時進行類型擦除后,則T屬性將默認為extends繼承的父類中最左邊一個,這里即為Integer;而<T super Integer>描述的類,在進行類型擦除后將無法確定其類型。
根據(jù)以上兩點,在類的描述中,不能使用<T super Integer>的形式限定參數(shù)類型的下界。
通配符的上下界使用有
PECS(producer extends, consumer super)原則,producer可以根據(jù)上界進行元素讀取,但是不確定類型,所以無法添加元素;consumer可以根據(jù)下界進行元素添加,但是不確定類型,所以無法讀取元素。
泛型數(shù)組
在普通數(shù)組的使用中,存在如下的情況:
public class Test {
public static void main(String[] args) {
Integer[] integers = new Integer[5];
Object[] objects = integers;
objects[0] = "abc";
}
}
這段代碼在編譯期是沒問題的,在運行時會報出ArrayStoreException異常。這種情況稱之為數(shù)組的協(xié)變(covariant),即S類型為T類型的子類型,則S類型數(shù)組為T類型數(shù)組的子類型。
為了避免這種協(xié)變的情況發(fā)生,Java禁止創(chuàng)建具體類型的泛型數(shù)組,否則對于泛型數(shù)組有如下情況,示例來源Java 指導手冊:
// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0);
如果Java中允許創(chuàng)建具體類型的泛型數(shù)組,則以上代碼在編譯期通過類型檢查,在運行期獲取元素時會報出ClassCastException異常,即無法通過泛型元素的隱式類型轉(zhuǎn)換。
Java雖然禁止創(chuàng)建具體類型的泛型數(shù)組,但并不禁止創(chuàng)建通配符形式的數(shù)組,如下所示,示例來源Java 指導手冊:
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
雖然發(fā)生運行期錯誤,但是因為通配符的使用,所以在獲取元素時,需要進行顯示類型轉(zhuǎn)換,也就是將元素的類型操作交給開發(fā)人員進行控制。
參考
Type Parameters
Difference between <? super T> and <? extends T> in Java
The Java? Tutorials