泛型概述
Java泛型(generics)是JDK 5中引入的一個新特性稀并,允許在定義類和接口的時候使用類型參數(shù)(type parameter)。聲明的類型參數(shù)在使用時用具體的類型來替換天试。
優(yōu)缺點(diǎn)
? ? ? ?從好的方面來說氛什,泛型的引入可以解決之前的集合類框架在使用過程中通常會出現(xiàn)的運(yùn)行時刻類型錯誤朴上,因?yàn)榫幾g器可以在編譯時刻就發(fā)現(xiàn)很多明顯的錯誤秦踪。而從不好的地方來說,為了保證與舊有版本的兼容性爬范,Java泛型的實(shí)現(xiàn)上存在著一些不夠優(yōu)雅的地方父腕。當(dāng)然這也是任何有歷史的編程語言所需要承擔(dān)的歷史包袱。后續(xù)的版本更新會為早期的設(shè)計缺陷所累青瀑。
舉例
? ? ? ?List(Object)作為形式參數(shù)璧亮,那么如果嘗試將一個List(String)的對象作為實(shí)際參數(shù)傳進(jìn)去,卻發(fā)現(xiàn)無法通過編譯斥难。雖然從直覺上來說枝嘶,Object是String的父類,這種類型轉(zhuǎn)換應(yīng)該是合理的哑诊。但是實(shí)際上這會產(chǎn)生隱含的類型轉(zhuǎn)換問題群扶,因此編譯器直接就禁止這樣的行為。
類型擦除
正確理解泛型概念的首要前提是理解類型擦除(type erasure)搭儒。
Java中的泛型基本上都是在編譯器這個層次來實(shí)現(xiàn)的穷当。在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的提茁。使用泛型的時候加上的類型參數(shù)淹禾,會被編譯器在編譯的時候去掉。這個過程就稱為類型擦除茴扁。如在代碼中定義的List(Object)和List(String)等類型铃岔,在編譯之后都會變成List。JVM看到的只是List峭火,而由泛型附加的類型信息對JVM來說是不可見的毁习。Java編譯器會在編譯時盡可能的發(fā)現(xiàn)可能出錯的地方,但是仍然無法避免在運(yùn)行時刻出現(xiàn)類型轉(zhuǎn)換異常的情況卖丸。類型擦除也是Java的泛型實(shí)現(xiàn)方式與C++模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別纺且。
很多泛型的奇怪特性都與這個類型擦除的存在有關(guān)
1.泛型類并沒有自己獨(dú)有的Class類對象。比如并不存在List(String).class或是List(Integer).class稍浆,而只有List.class载碌。
2.靜態(tài)變量是被泛型類的所有實(shí)例所共享的猜嘱。對于聲明為MyClass(T)的類,訪問其中的靜態(tài)變量的方法仍然是 MyClass.myStaticVar嫁艇。不管是通過new MyClass(String)還是new MyClass(Integer)創(chuàng)建的對象朗伶,都是共享一個靜態(tài)變量。
3.泛型的類型參數(shù)不能用在Java異常處理的catch語句中步咪。因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時刻來進(jìn)行的论皆。由于類型信息被擦除,JVM是無法區(qū)分兩個異常類型MyException(String)和MyException(Integer)的猾漫。對于JVM來說点晴,它們都是 MyException類型的。也就無法執(zhí)行與異常對應(yīng)的catch語句悯周。
類型擦除的過程
類型擦除的基本過程也比較簡單觉鼻,首先是找到用來替換類型參數(shù)的具體類。這個具體類一般是Object队橙。如果指定了類型參數(shù)的上界的話坠陈,則使用這個上界。把代碼中的類型參數(shù)都替換成具體的類捐康。同時去掉出現(xiàn)的類型聲明仇矾,即去掉<>的內(nèi)容。比如T get()方法聲明就變成了Object get()解总;List(String)就變成了List贮匕。接下來就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法花枫。
實(shí)例分析
了解了類型擦除機(jī)制之后刻盐,就會明白編譯器承擔(dān)了全部的類型檢查工作。編譯器禁止某些泛型的使用方式劳翰,正是為了確保類型的安全性敦锌。以上面提到的List(Object)和List(String)為例來具體分析:
泛型類
容器類應(yīng)該算得上最具重用性的類庫之一崔列。先來看一個沒有泛型的情況下的容器類如何定義:
public class Container {
? ? private String key;
? ? private String value;
? ? public Container(String k, String v) {
? ? ? ? key = k;
? ? ? ? value = v;
? ? }
? ? public String getKey() {
? ? ? ? return key;
? ? }
? ? public void setKey(String key) {
? ? ? ? this.key = key;
? ? }
? ? public String getValue() {
? ? ? ? return value;
? ? }
? ? public void setValue(String value) {
? ? ? ? this.value = value;
? ? }
}
Container類保存了一對key-value鍵值對梢褐,但是類型是定死的,也就說如果我想要創(chuàng)建一個鍵值對是String-Integer類型的赵讯,當(dāng)前這個Container是做不到的盈咳,必須再自定義。那么這明顯重用性就非常低边翼。當(dāng)然鱼响,我可以用Object來代替String,并且在Java SE5之前组底,我們也只能這么做丈积,由于Object是所有類型的基類,所以可以直接轉(zhuǎn)型债鸡。但是這樣靈活性還是不夠江滨,因?yàn)檫€是指定類型了,只不過這次指定的類型層級更高而已厌均,有沒有可能不指定類型唬滑?有沒有可能在運(yùn)行時才知道具體的類型是什么?
所以棺弊,就出現(xiàn)了泛型晶密。
public class Container(K, V) {
? ? private K key;
? ? private V value;
? ? public Container(K k, V v) {
? ? ? ? key = k;
? ? ? ? value = v;
? ? }
? ? public K getKey() {
? ? ? ? return key;
? ? }
? ? public void setKey(K key) {
? ? ? ? this.key = key;
? ? }
? ? public V getValue() {
? ? ? ? return value;
? ? }
? ? public void setValue(V value) {
? ? ? ? this.value = value;
? ? }
}
在編譯期,是無法知道K和V具體是什么類型模她,只有在運(yùn)行時才會真正根據(jù)類型來構(gòu)造和分配內(nèi)存稻艰。可以看一下現(xiàn)在Container類對于不同類型的支持情況:
public class Main {
? ? public static void main(String[] args) {
? ? ? ? Container<String, String> c1 = new Container<String, String>("name", "findingsea");
? ? ? ? Container<String, Integer> c2 = new Container<String, Integer>("age", 24);
? ? ? ? Container<Double, Double> c3 = new Container<Double, Double>(1.1, 2.2);
? ? ? ? System.out.println(c1.getKey() + " : " + c1.getValue());
? ? ? ? System.out.println(c2.getKey() + " : " + c2.getValue());
? ? ? ? System.out.println(c3.getKey() + " : " + c3.getValue());
? ? }
}
輸出:
name : findingsea
age : 24
1.1 : 2.2
泛型接口
在泛型接口中缝驳,生成器是一個很好的理解连锯,看如下的生成器接口定義:
public interface Generator<T> {
? ? public T next();
}
然后定義一個生成器類來實(shí)現(xiàn)這個接口:
public class FruitGenerator implements Generator<String> {
? ? private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
? ? @Override
? ? public String next() {
? ? ? ? Random rand = new Random();
? ? ? ? return fruits[rand.nextInt(3)];
? ? }
}
調(diào)用:
public class Main {
? ? public static void main(String[] args) {
? ? ? ? FruitGenerator generator = new FruitGenerator();
? ? ? ? System.out.println(generator.next());
? ? ? ? System.out.println(generator.next());
? ? ? ? System.out.println(generator.next());
? ? ? ? System.out.println(generator.next());
? ? }
}
輸出:
Banana
Banana
Pear
Banana
泛型方法
一個基本的原則是:無論何時归苍,只要你能做到用狱,你就應(yīng)該盡量使用泛型方法。也就是說拼弃,如果使用泛型方法可以取代將整個類泛化夏伊,那么應(yīng)該有限采用泛型方法。下面來看一個簡單的泛型方法的定義:
public class Main {
? ? public static <T> void out(T t) {
? ? ? ? System.out.println(t);
? ? }
? ? public static void main(String[] args) {
? ? ? ? out("findingsea");
? ? ? ? out(123);
? ? ? ? out(11.11);
? ? ? ? out(true);
? ? }
}
可以看到方法的參數(shù)徹底泛化了吻氧,這個過程涉及到編譯器的類型推導(dǎo)和自動打包溺忧,也就說原來需要我們自己對類型進(jìn)行的判斷和處理咏连,現(xiàn)在編譯器幫我們做了。這樣在定義方法的時候不必考慮以后到底需要處理哪些類型的參數(shù)鲁森,大大增加了編程的靈活性祟滴。
再看一個泛型方法和可變參數(shù)的例子:
public class Main {
? ? public static <T> void out(T... args) {
? ? ? ? for (T t : args) {
? ? ? ? ? ? System.out.println(t);
? ? ? ? }
? ? }
? ? public static void main(String[] args) {
? ? ? ? out("findingsea", 123, 11.11, true);
? ? }
}
通配符與上下界
在使用泛型類的時候,既可以指定一個具體的類型歌溉,如List(String)就聲明了具體的類型是String垄懂;也可以用通配符?來表示未知類型,如List就聲明了List中包含的元素類型是未知的痛垛。 通配符所代表的其實(shí)是一組類型,但具體的類型是未知的。List所聲明的就是所有類型都是可以的桶错。但是List并不等同于List(Object)桃移。List(Object)實(shí)際上確定了List中包含的是Object及其子類,在使用的時候都可以通過Object來進(jìn)行引用蹂析。而List則其中所包含的元素類型是不確定舔示。其中可能包含的是String,也可能是 Integer电抚。如果它包含了String的話斩郎,往里面添加Integer類型的元素就是錯誤的。正因?yàn)轭愋臀粗髌担筒荒芡ㄟ^new ArrayList(?)()的方法來創(chuàng)建一個新的ArrayList對象缩宜。因?yàn)榫幾g器無法知道具體的類型是什么。但是對于 List(?)中的元素確總是可以用Object來引用的甥温,因?yàn)殡m然類型未知锻煌,但肯定是Object及其子類∫鲵荆考慮下面的代碼:
public void wildcard(List(?) list) {list.add(1);//編譯錯誤}
如上所示宋梧,試圖對一個帶通配符的泛型類進(jìn)行操作的時候,總是會出現(xiàn)編譯錯誤狰挡。其原因在于通配符所表示的類型是未知的捂龄。
因?yàn)閷τ贚ist(?)中的元素只能用Object來引用,在有些情況下不是很方便加叁。在這些情況下倦沧,可以使用上下界來限制未知類型的范圍。 如List(? extends Number)說明List中可能包含的元素類型是Number及其子類它匕。而List(? super Number)則說明List中包含的是Number及其父類展融。當(dāng)引入了上界之后,在使用類型的時候就可以使用上界類中定義的方法豫柬。比如訪問 List(? extends Number)的時候告希,就可以使用Number類的intValue等方法扑浸。
類型系統(tǒng)
在Java中,大家比較熟悉的是通過繼承機(jī)制而產(chǎn)生的類型體系結(jié)構(gòu)燕偶。比如String繼承自O(shè)bject喝噪。根據(jù)Liskov替換原則,子類是可以替換父類的指么。當(dāng)需要Object類的引用的時候仙逻,如果傳入一個String對象是沒有任何問題的。但是反過來的話涧尿,即用父類的引用替換子類引用的時候系奉,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。編譯器并不能保證運(yùn)行時刻這種轉(zhuǎn)換一定是合法的姑廉。這種自動的子類替換父類的類型轉(zhuǎn)換機(jī)制缺亮,對于數(shù)組也是適用的。 String[]可以替換Object[]桥言。但是泛型的引入萌踱,對于這個類型系統(tǒng)產(chǎn)生了一定的影響。正如前面提到的List(String)是不能替換掉List(Object)的号阿。
引入泛型之后的類型系統(tǒng)增加了兩個維度:
一個是類型參數(shù)自身的繼承體系結(jié)構(gòu)并鸵,另外一個是泛型類或接口自身的繼承體系結(jié)構(gòu)。第一個指的是對于 List(String)和List(Object)這樣的情況扔涧,類型參數(shù)String是繼承自O(shè)bject的园担。而第二種指的是 List接口繼承自Collection接口。對于這個類型系統(tǒng)枯夜,有如下的一些規(guī)則:
相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)弯汰。
即List(String)是Collection(String) 的子類型,List(String)可以替換Collection(String)湖雹。這種情況也適用于帶有上下界的類型聲明咏闪。
當(dāng)泛型類的類型聲明中使用了通配符的時候, 其子類型可以在兩個維度上分別展開摔吏。如對Collection(? extends Number)來說鸽嫂,其子類型可以在Collection這個維度上展開,即List(? extends Number)和Set(? extends Number)等征讲;也可以在Number這個層次上展開据某,即Collection(Double)和 Collection(Integer)等。如此循環(huán)下去稳诚,ArrayList(Long)和 HashSet(Double)等也都算是Collection(? extends Number)的子類型哗脖。
如果泛型類中包含多個類型參數(shù),則對于每個類型參數(shù)分別應(yīng)用上面的規(guī)則扳还。
理解了上面的規(guī)則之后才避,就可以很容易的修正實(shí)例分析中給出的代碼了。只需要把List(Object)改成List(?)即可氨距。List(String)是List(?)的子類型桑逝,因此傳遞參數(shù)時不會發(fā)生錯誤。
泛型的命名規(guī)范
為了更好地去理解泛型俏让,我們也需要去理解java泛型的命名規(guī)范楞遏。為了與java關(guān)鍵字區(qū)別開來,java泛型參數(shù)只是使用一個大寫字母來定義首昔。各種常用泛型參數(shù)的意義如下:
E — Element寡喝,常用在java Collection里,如:List(E),Iterator(E),Set(E)
K,V — Key勒奇,Value预鬓,代表Map的鍵值對
N — Number,數(shù)字
T — Type赊颠,類型格二,如String,Integer等等