Java泛型解析(太過精辟)

泛型概述

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)為例來具體分析:


這段代碼中,inspect方法接受List(Object)作為參數(shù)佳簸,當(dāng)在test方法中試圖傳入List(String)的時候乙墙,會出現(xiàn)編譯錯誤。假設(shè)這樣的做法是允許的生均,那么在inspect方法就可以通過list.add(1)來向集合中添加一個數(shù)字听想。這樣在test方法看來,其聲明為List(String)的集合中卻被添加了一個Integer類型的對象马胧。這顯然是違反類型安全的原則的汉买,在某個時候肯定會拋出ClassCastException。因此佩脊,編譯器禁止這樣的行為蛙粘。編譯器會盡可能的檢查可能存在的類型安全問題朽色。對于確定是違反相關(guān)原則的地方,會給出編譯錯誤组题。當(dāng)編譯器無法判斷類型的使用是否正確的時候葫男,會給出警告信息。

泛型類

容器類應(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等等

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末竣蹦,一起剝皮案震驚了整個濱河市顶猜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痘括,老刑警劉巖长窄,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纲菌,居然都是意外死亡抄淑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門驰后,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肆资,“玉大人,你說我怎么就攤上這事灶芝≈T” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵夜涕,是天一觀的道長犯犁。 經(jīng)常有香客問我,道長女器,這世上最難降的妖魔是什么酸役? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上涣澡,老公的妹妹穿的比我還像新娘贱呐。我一直安慰自己,他們只是感情好入桂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布奄薇。 她就那樣靜靜地躺著,像睡著了一般抗愁。 火紅的嫁衣襯著肌膚如雪馁蒂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天蜘腌,我揣著相機(jī)與錄音沫屡,去河邊找鬼。 笑死撮珠,一個胖子當(dāng)著我的面吹牛沮脖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播劫瞳,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼倘潜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了志于?” 一聲冷哼從身側(cè)響起涮因,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伺绽,沒想到半個月后养泡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奈应,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年澜掩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杖挣。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡肩榕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惩妇,到底是詐尸還是另有隱情株汉,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布歌殃,位于F島的核電站乔妈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氓皱。R本人自食惡果不足惜路召,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一勃刨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧股淡,春花似錦身隐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躲因。三九已至早敬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間大脉,已是汗流浹背搞监。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镰矿,地道東北人琐驴。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像秤标,于是被迫代替她去往敵國和親绝淡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353