文章作者:Tyan
博客:noahsnail.com
1. 什么是泛型
Java泛型(Generics)是JDK 5中引入的一個(gè)新特性,允許在定義類和接口的時(shí)候使用類型參數(shù)(type parameter),它們也被稱為參數(shù)化類型(parameterized type)或參量多態(tài)(parametric polymorphism)。泛型最主要的應(yīng)用是在JDK 5中的新集合類框架中。Java泛型的應(yīng)用可以提高代碼的復(fù)用性,同時(shí)泛型提供了類型檢查,減少了數(shù)據(jù)的類型轉(zhuǎn)換,保證了編譯時(shí)的類型安全。
1.1 Java泛型的優(yōu)點(diǎn)
Java泛型保持了和Java語(yǔ)言以及Java虛擬機(jī)很好的兼容性,下面對(duì)Java泛型的特點(diǎn)做一個(gè)簡(jiǎn)要的概:
類型安全。 泛型的一個(gè)主要目標(biāo)就是提高Java程序的類型安全。使用泛型可以使編譯器知道變量的類型限制,進(jìn)而可以在更高程度上驗(yàn)證類型假設(shè)。如果沒有泛型,那么類型的安全性主要由程序員來(lái)把握,這顯然不如帶有泛型的程序安全性高。
消除強(qiáng)制類型轉(zhuǎn)換。泛型可以消除源代碼中的許多強(qiáng)制類型轉(zhuǎn)換,這樣可以使代碼更加可讀,并減少出錯(cuò)的機(jī)會(huì)。
向后兼容。支持泛型的Java編譯器(例如JDK5.0中的Javac)可以用來(lái)編譯經(jīng)過(guò)泛型擴(kuò)充的Java程序(Java泛型程序),但是現(xiàn)有的沒有使用泛型擴(kuò)充的Java程序仍然可以用這些編譯器來(lái)編譯。
層次清晰,恪守規(guī)范。無(wú)論被編譯的源程序是否使用泛型擴(kuò)充,編譯生成的字節(jié)碼均可被虛擬機(jī)接受并執(zhí)行。也就是說(shuō)不管編譯器的輸入是Java泛型程序,還是一般的Java程序,經(jīng)過(guò)編譯后的字節(jié)碼都嚴(yán)格遵循《Java虛擬機(jī)規(guī)范》中對(duì)字節(jié)碼的要求??梢姡盒椭饕窃诰幾g器層面實(shí)現(xiàn)的,它對(duì)于 Java 虛擬機(jī)是透明的。
性能收益。目前來(lái)講,用Java泛型編寫的代碼和一般的Java代碼在效率上是非常接近的。 但是由于泛型會(huì)給Java編譯器和虛擬機(jī)帶來(lái)更多的類型信息,因此利用這些信息對(duì)Java程序做進(jìn)一步優(yōu)化將成為可能。
1.2 使用Java泛型要注意的事項(xiàng)
在使用泛型的時(shí)候可以遵循一些基本的原則,從而避免一些常見的問題。
在代碼中避免泛型類和原始類型的混用。比如List<String>和List不應(yīng)該共同使用。這樣會(huì)產(chǎn)生一些編譯器警告和潛在的運(yùn)行時(shí)異常。當(dāng)需要利用JDK 5之前開發(fā)的遺留代碼,而不得不這么做時(shí),也盡可能的隔離相關(guān)的代碼。
在使用帶通配符的泛型類的時(shí)候,需要明確通配符所代表的一組類型的概念。由于具體的類型是未知的,很多操作是不允許的。
泛型類最好不要同數(shù)組一塊使用。你只能創(chuàng)建new List<?>[10]這樣的數(shù)組,無(wú)法創(chuàng)建new List<String>[10]這樣的。這限制了數(shù)組的使用能力,而且會(huì)帶來(lái)很多費(fèi)解的問題。因此,當(dāng)需要類似數(shù)組的功能時(shí)候,使用集合類即可。
不要忽視編譯器給出的警告信息。
2. 與泛型相關(guān)的一些概念
2.1 類型擦除
正確理解泛型概念的首要前提是理解類型擦除(type erasure)。 Java中的泛型基本上都是在編譯器這個(gè)層次來(lái)實(shí)現(xiàn)的。在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的。使用泛型的時(shí)候加上的類型參數(shù),會(huì)被編譯器在編譯的時(shí)候去掉。這個(gè)過(guò)程就稱為類型擦除。如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會(huì)變成List。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來(lái)說(shuō)是不可見的。Java編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方,但是仍然無(wú)法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。類型擦除也是Java的泛型實(shí)現(xiàn)方式與C++模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。
很多泛型的奇怪特性都與這個(gè)類型擦除的存在有關(guān),包括:
泛型類并沒有自己獨(dú)有的Class類對(duì)象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
靜態(tài)變量是被泛型類的所有實(shí)例所共享的。對(duì)于聲明為MyClass<T>的類,訪問其中的靜態(tài)變量的方法仍然是 MyClass.myStaticVar。不管是通過(guò)new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對(duì)象,都是共享一個(gè)靜態(tài)變量。
泛型的類型參數(shù)不能用在Java異常處理的catch語(yǔ)句中。因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時(shí)刻來(lái)進(jìn)行的。由于類型信息被擦除,JVM是無(wú)法區(qū)分兩個(gè)異常類型MyException<String>和MyException<Integer>的。對(duì)于JVM來(lái)說(shuō),它們都是 MyException類型的。也就無(wú)法執(zhí)行與異常對(duì)應(yīng)的catch語(yǔ)句。
類型擦除的基本過(guò)程也比較簡(jiǎn)單,首先是找到用來(lái)替換類型參數(shù)的具體類。這個(gè)具體類一般是Object。如果指定了類型參數(shù)的上界的話,則使用這個(gè)上界。把代碼中的類型參數(shù)都替換成具體的類。同時(shí)去掉出現(xiàn)的類型聲明,即去掉<>的內(nèi)容。比如T get()方法聲明就變成了Object get();List<String>就變成了List。接下來(lái)就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法。比如考慮下面的代碼:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
當(dāng)類型信息被擦除之后,上述類的聲明變成了class MyString implements Comparable。但是這樣的話,類MyString就會(huì)有編譯錯(cuò)誤,因?yàn)闆]有實(shí)現(xiàn)接口Comparable聲明的int compareTo(Object)方法。這個(gè)時(shí)候就由編譯器來(lái)動(dòng)態(tài)生成這個(gè)方法。
了解了類型擦除機(jī)制之后,就會(huì)明白編譯器承擔(dān)了全部的類型檢查工作。編譯器禁止某些泛型的使用方式,正是為了確保類型的安全性。以上面提到的List<Object>和List<String>為例來(lái)具體分析:
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); //這個(gè)操作在當(dāng)前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); //編譯錯(cuò)誤
}
這段代碼中,inspect方法接受List<Object>作為參數(shù),當(dāng)在test方法中試圖傳入List<String>的時(shí)候,會(huì)出現(xiàn)編譯錯(cuò)誤。假設(shè)這樣的做法是允許的,那么在inspect方法就可以通過(guò)list.add(1)來(lái)向集合中添加一個(gè)數(shù)字。這樣在test方法看來(lái),其聲明為L(zhǎng)ist<String>的集合中卻被添加了一個(gè)Integer類型的對(duì)象。這顯然是違反類型安全的原則的,在某個(gè)時(shí)候肯定會(huì)拋出ClassCastException。因此,編譯器禁止這樣的行為。編譯器會(huì)盡可能的檢查可能存在的類型安全問題。對(duì)于確定是違反相關(guān)原則的地方,會(huì)給出編譯錯(cuò)誤。當(dāng)編譯器無(wú)法判斷類型的使用是否正確的時(shí)候,會(huì)給出警告信息。
2.2 通配符與上下界
在使用泛型類的時(shí)候,既可以指定一個(gè)具體的類型,如List<String>就聲明了具體的類型是String;也可以用通配符?來(lái)表示未知類型,如List<?>就聲明了List中包含的元素類型是未知的。 通配符所代表的其實(shí)是一組類型,但具體的類型是未知的。List<?>所聲明的就是所有類型都是可以的。但是List<?>并不等同于List<Object>。List<Object>實(shí)際上確定了List中包含的是Object及其子類,在使用的時(shí)候都可以通過(guò)Object來(lái)進(jìn)行引用。而List<?>則其中所包含的元素類型是不確定。其中可能包含的是String,也可能是 Integer。如果它包含了String的話,往里面添加Integer類型的元素就是錯(cuò)誤的。正因?yàn)轭愋臀粗?,就不能通過(guò)new ArrayList<?>()的方法來(lái)創(chuàng)建一個(gè)新的ArrayList對(duì)象。因?yàn)榫幾g器無(wú)法知道具體的類型是什么。但是對(duì)于 List<?>中的元素卻總是可以用Object來(lái)引用的,因?yàn)殡m然類型未知,但肯定是Object及其子類。考慮下面的代碼:
public void wildcard(List<?> list) {
list.add(1);//編譯錯(cuò)誤
}
如上所示,試圖對(duì)一個(gè)帶通配符的泛型類進(jìn)行操作的時(shí)候,總是會(huì)出現(xiàn)編譯錯(cuò)誤。其原因在于通配符所表示的類型是未知的。
因?yàn)閷?duì)于List<?>中的元素只能用Object來(lái)引用,在有些情況下不是很方便。在這些情況下,可以使用上下界來(lái)限制未知類型的范圍。 如List<? extends Number>說(shuō)明List中可能包含的元素類型是Number及其子類。而List<? super Number>則說(shuō)明List中包含的是Number及其父類。當(dāng)引入了上界之后,在使用類型的時(shí)候就可以使用上界類中定義的方法。比如訪問 List<? extends Number>的時(shí)候,就可以使用Number類的intValue等方法。
2.3 類型系統(tǒng)
在Java中,大家比較熟悉的是通過(guò)繼承機(jī)制而產(chǎn)生的類型體系結(jié)構(gòu)。比如String繼承自O(shè)bject。根據(jù)Liskov替換原則,子類是可以替換父類的。當(dāng)需要Object類的引用的時(shí)候,如果傳入一個(gè)String對(duì)象是沒有任何問題的。但是反過(guò)來(lái)的話,即用父類的引用替換子類引用的時(shí)候,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。編譯器并不能保證運(yùn)行時(shí)刻這種轉(zhuǎn)換一定是合法的。這種自動(dòng)的子類替換父類的類型轉(zhuǎn)換機(jī)制,對(duì)于數(shù)組也是適用的。 String[]可以替換Object[]。但是泛型的引入,對(duì)于這個(gè)類型系統(tǒng)產(chǎn)生了一定的影響。正如前面提到的List<String>是不能替換掉List<Object>的。
引入泛型之后的類型系統(tǒng)增加了兩個(gè)維度:一個(gè)是類型參數(shù)自身的繼承體系結(jié)構(gòu),另外一個(gè)是泛型類或接口自身的繼承體系結(jié)構(gòu)。第一個(gè)指的是對(duì)于 List<String>和List<Object>這樣的情況,類型參數(shù)String是繼承自O(shè)bject的。而第二種指的是 List接口繼承自Collection接口。對(duì)于這個(gè)類型系統(tǒng),有如下的一些規(guī)則:
相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)。即List<String>是Collection<String> 的子類型,List<String>可以替換Collection<String>。這種情況也適用于帶有上下界的類型聲明。
當(dāng)泛型類的類型聲明中使用了通配符的時(shí)候,其子類型可以在兩個(gè)維度上分別展開。如對(duì)Collection<? extends Number>來(lái)說(shuō),其子類型可以在Collection這個(gè)維度上展開,即List<? extends Number>和Set<? extends Number>等;也可以在Number這個(gè)層次上展開,即Collection<Double>和 Collection<Integer>等。如此循環(huán)下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子類型。
如果泛型類中包含多個(gè)類型參數(shù),則對(duì)于每個(gè)類型參數(shù)分別應(yīng)用上面的規(guī)則。
理解了上面的規(guī)則之后,就可以很容易的修正實(shí)例分析中給出的代碼了。只需要把List<Object>改成List<?>即可。List<String>是List<?>的子類型,因此傳遞參數(shù)時(shí)不會(huì)發(fā)生錯(cuò)誤。
2.4 橋方法
http://www.cnblogs.com/ggjucheng/p/3352519.html
3. 泛型類
一個(gè)典型的泛型類的例子是ArrayList類:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}
用法就不用多說(shuō)了。
4. 泛型接口
一個(gè)典型的泛型接口例子是Comparable接口:
public interface Comparable<T> {
...
public int compareTo(T o);
...
}
用法:
public class Test implements Comparable<String>{
@Override
public int compareTo(String o) {
return 0;
}
}
5. 泛型方法
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
6. 泛型命名規(guī)范
為了更好地去理解泛型,我們也需要去理解Java泛型的命名規(guī)范。為了與Java關(guān)鍵字區(qū)別開來(lái),Java泛型參數(shù)只是使用一個(gè)大寫字母來(lái)定義。各種常用泛型參數(shù)的意義如下:
E — Element,常用在Java Collection里,如:List<E>,Iterator<E>,Set<E>
K,V — Key,Value,代表Map的鍵值對(duì)
N — Number,數(shù)字
T — Type,類型,如String,Integer等等
參考資料:
1、http://www.infoq.com/cn/articles/cf-java-generics
2、https://www.ibm.com/developerworks/cn/java/j-lo-gj/
3、http://peiquan.blog.51cto.com/7518552/1302898
4、https://docs.oracle.com/javase/tutorial/java/generics/
5、Effective Java 2.0