第07部分:Java泛型

Java 平臺(tái)的一大優(yōu)勢(shì)是它提供的標(biāo)準(zhǔn)庫(kù)。標(biāo)準(zhǔn)庫(kù)提供了大量有用的功能,特別是實(shí)現(xiàn)了健壯的通用數(shù)據(jù)結(jié)構(gòu)疏旨。這些實(shí)現(xiàn)使用起來(lái)相當(dāng)簡(jiǎn)單,而且文檔編寫良好扎酷。這些是 Java 集合庫(kù)檐涝,后面會(huì)介紹。

雖然這些庫(kù)一直很有用法挨,但在早期版本中有相當(dāng)大的不足——數(shù)據(jù)結(jié)構(gòu)(經(jīng)常叫作容器)完全隱藏了存儲(chǔ)其中的數(shù)據(jù)類型谁榜。

數(shù)據(jù)隱藏和封裝是面向?qū)ο缶幊痰闹匾瓌t,但在這種情況下凡纳,容器的不透明會(huì)為開發(fā)者帶來(lái)很多問題窃植。

本文先說明這個(gè)問題,然后介紹泛型是如何解決這個(gè)問題并讓 Java 開發(fā)者的生活更輕松的荐糜。

介紹泛型

如果想構(gòu)建一個(gè)由 Shape 實(shí)例組成的集合巷怜,可以把這個(gè)集合保存在一個(gè) List 對(duì)象中葛超,如下所示:

上述代碼有個(gè)問題,為了取回有用的形狀對(duì)象形式延塑,必須校正绣张,因?yàn)?List 不知道其中的對(duì)象是什么類型。不僅如此页畦,其實(shí)可以把不同類型的對(duì)象放在同一個(gè)容器中胖替,一切都能正常運(yùn)行,但是如果做了不合法的校正豫缨,程序就會(huì)崩潰独令。

我們真正需要的是一種知道所含元素類型的 List。這樣好芭,如果把不合法的參數(shù)傳給 List的方法燃箭,javac 就能檢測(cè)到,導(dǎo)致編譯出錯(cuò)舍败,而不用等到運(yùn)行時(shí)才發(fā)現(xiàn)問題招狸。

為了解決這個(gè)問題,Java 提供了一種句法邻薯,指明某種類型是一個(gè)容器裙戏,這個(gè)容器中保存著其他引用類型的實(shí)例。容器中保存的負(fù)載類型(payload type)在尖括號(hào)中指定:

這種句法能讓編譯器捕獲大量不安全的代碼厕诡,根本不能靠近運(yùn)行時(shí)累榜。當(dāng)然,這正是靜態(tài)類型系統(tǒng)的關(guān)鍵所在——使用編譯時(shí)信息協(xié)助排除大量運(yùn)行時(shí)問題灵嫌。

容器類型一般叫作泛型(generic type)壹罚,使用下述方式聲明:

上述代碼表明,Box 接口是通用結(jié)構(gòu)寿羞,可以保存任意類型的負(fù)載猖凛。這不是一個(gè)完整的接口,更像是一系列接口的通用描述绪穆,每個(gè)接口對(duì)應(yīng)的類型都能用在 T 的位置上辨泳。


泛型和類型參數(shù)

我們已經(jīng)知道如何使用泛型增強(qiáng)程序的安全性——使用編譯時(shí)信息避免簡(jiǎn)單的類型錯(cuò)誤。下面深入介紹泛型的特性霞幅。

<T> 句法有個(gè)專門的名稱——類型參數(shù)(type parameter)漠吻。因此,泛型還有一個(gè)名稱——參數(shù)化類型(parameterized type)司恳。這表明途乃,容器類型(例如 List)由其他類型(負(fù)載類型)參數(shù)化。把類型寫為 Map<String, Integer> 時(shí)扔傅,我們就為類型參數(shù)指定了具體的值耍共。

定義有參數(shù)的類型時(shí)烫饼,要使用一種不對(duì)類型參數(shù)做任何假設(shè)的方式指定具體的值。所以List 類型使用通用的方式 List<E> 聲明试读,而且自始至終都使用類型參數(shù) E 作占位符杠纵,代表程序員使用 List 數(shù)據(jù)結(jié)構(gòu)時(shí)負(fù)載的真實(shí)類型。類型參數(shù)始終代表引用類型钩骇。類型參數(shù)的值不能使用基本類型比藻。

類型參數(shù)可以在方法的簽名和主體中使用,就像是真正的類型一樣倘屹,例如:

注意银亲,類型參數(shù) E 既可以作為返回類型的參數(shù),也可以作為方法參數(shù)類型的參數(shù)纽匙。我們不假設(shè)負(fù)載類型有任何具體的特性务蝠,只對(duì)一致性做了基本假設(shè),即存入的類型和后來(lái)取回的類型一致烛缔。


菱形句法

創(chuàng)建泛型的實(shí)例時(shí)馏段,賦值語(yǔ)句的右側(cè)會(huì)重復(fù)類型參數(shù)的值。一般情況下践瓷,這個(gè)信息是不必要的院喜,因?yàn)榫幾g器能推導(dǎo)出類型參數(shù)的值。在 Java 的現(xiàn)代版本中晕翠,可以使用菱形句法省略重復(fù)的類型值够坐。

下面通過一個(gè)示例說明如何使用菱形句法,這個(gè)例子改自之前的示例:

對(duì)這種冗長(zhǎng)的賦值語(yǔ)句來(lái)說崖面,這是個(gè)小改進(jìn),能少輸入幾個(gè)字符梯影。本章末尾介紹 lambda表達(dá)式時(shí)會(huì)再次討論類型推導(dǎo)巫员。


類型擦除

Java 平臺(tái)十分看重向后兼容性。Java 5 添加的泛型又是一個(gè)會(huì)導(dǎo)致向后兼容性問題的新語(yǔ)言特性甲棍。

問題的關(guān)鍵是简识,如何讓類型系統(tǒng)既能使用舊的非泛型集合類又能使用新的泛型集合類。設(shè)計(jì)者選擇的解決方式是使用校正:

上述代碼表明感猛,作為類型七扰,List 和 List<String> 是兼容的,至少在某種程度上是兼容的陪白。Java 通過類型擦除實(shí)現(xiàn)這種兼容性颈走。這表明,泛型的類型參數(shù)只在編譯時(shí)可見——javac會(huì)去掉類型參數(shù)咱士,而且在字節(jié)碼中不體現(xiàn)出來(lái)立由。

非泛型的 List 一般叫作原始類型(raw type)轧钓。就算現(xiàn)在有泛型了,Java 也完全能處理類型的原始形式锐膜。不過毕箍,這么做幾乎就表明代碼的質(zhì)量不高。

類型擦除機(jī)制擴(kuò)大了 javac 和 JVM 使用的類型系統(tǒng)之間的區(qū)別道盏。類型擦除還能禁止使用某些其他定義方式而柑,如果沒有這個(gè)機(jī)制,代碼看起來(lái)是合法的荷逞。在

下述代碼中媒咳,我們想使用兩個(gè)稍微不同的數(shù)據(jù)結(jié)構(gòu)計(jì)算訂單數(shù)量:

看起來(lái)這是完全合法的 Java 代碼,但其實(shí)無(wú)法編譯颅围。問題是伟葫,這兩個(gè)方法雖然看起來(lái)像是常規(guī)的重載,但擦除類型后院促,兩個(gè)方法的簽名都變成了:

擦除類型后剩下的只有容器的原始類型筏养,在這個(gè)例子中是 Map。運(yùn)行時(shí)無(wú)法通過簽名區(qū)分這兩個(gè)方法常拓,所以渐溶,Java 語(yǔ)言規(guī)范把這種句法列為不合法的句法。


通配符

參數(shù)化類型弄抬,例如 ArrayList<T>茎辐,不能實(shí)例化,即不能創(chuàng)建這種類型的實(shí)例掂恕。這是因?yàn)?<T> 是類型參數(shù)拖陆,只是真實(shí)類型的占位符。只有為類型參數(shù)提供具體的值之后(例如ArrayList<String>)懊亡,這個(gè)類型才算完整依啰,才能創(chuàng)建這種類型的對(duì)象。

如果編譯時(shí)不知道我們要使用什么類型店枣,就會(huì)出現(xiàn)問題速警。幸好,Java 類型系統(tǒng)能調(diào)解這種問題鸯两。在 Java 中闷旧,有“未知類型”這個(gè)明確的概念,使用 <?> 表示钧唐。這是一種最簡(jiǎn)單的Java 通配符類型(wildcard type)忙灼。

涉及未知類型的表達(dá)式可以這么寫:

這是完全有效的 Java 代碼——ArrayList<?> 和 ArrayList<T> 不一樣,前者是變量可以使用的完整類型逾柿。我們對(duì) mysteryList 的負(fù)載類型一無(wú)所知缀棍,但這對(duì)我們的代碼來(lái)說不是問題宅此。在用戶的代碼中使用未知類型時(shí),有些限制爬范。例如父腕,下面的代碼不會(huì)編譯:

原 因 很 簡(jiǎn) 單, 我 們 不 知 道 mysteryList 的 負(fù) 載 類 型青瀑。 例 如璧亮, 如 果 mysteryList 是ArrayList<String> 類型的實(shí)例,那么就不能把 Object 對(duì)象存入其中斥难。始終可以存入容器的唯一一個(gè)值是 null枝嘶,因?yàn)槲覀冎?null 可能是任何引用類型的值。

但這沒什么用哑诊,因此群扶,Java 語(yǔ)言規(guī)范禁止實(shí)例化負(fù)載為未知類型的容器類型,例如:

使用未知類型時(shí)有必要問這么一個(gè)問題:“List<String> 是 List<Object> 的子類型嗎?”即镀裤,能否編寫如下的代碼:

乍看起來(lái)竞阐,這么寫完全可行,因?yàn)?String 是 Object 的子類暑劝,所以我們知道集合中的任何一個(gè) String 類型元素都是有效的 Object 對(duì)象骆莹。不過,看看下述代碼:

既然 objects 的類型聲明為 List<Object>担猛,那么就能把 Object 實(shí)例存入其中幕垦。然而,這個(gè)實(shí)例保存的是字符串傅联,嘗試存入的 Object 對(duì)象與其不兼容先改,因此這個(gè)操作在運(yùn)行時(shí)會(huì)失敗。上述問題的答案是蒸走,雖然下述代碼是合法的(因?yàn)?String 類繼承 Object 類):

但并不意味著泛型容器類型對(duì)應(yīng)的語(yǔ)句也合法:

換種方式說盏道,即 List<String> 不是 List<Object> 的子類型。如果想讓容器的類型具有父子關(guān)系载碌,需要使用未知類型:

這表明,List<String> 是 List<?> 的子類型衅枫。不過嫁艇,使用上述這種賦值語(yǔ)句時(shí),會(huì)丟失一些類型信息弦撩。例如步咪,get() 方法的返回類型現(xiàn)在實(shí)際上是 Object。還要注意益楼,不管 T 的值是什么猾漫,List<?> 都不是 List<T> 的子類型点晴。未知類型有時(shí)會(huì)讓開發(fā)者困惑,問些引人深思的問題悯周,例如:“為什么不使用 Object 代替未知類型?”不過粒督,如前文所述,為了實(shí)現(xiàn)泛型之間的父子關(guān)系禽翼,必須有一種表示未知類型的方式屠橄。

1. 受限通配符

其實(shí),Java 的通配符類型不止有未知類型一種闰挡,還有受限通配符(bounded wildcard)這個(gè)概念锐墙。受限通配符也叫類型參數(shù)約束條件,作用是限制類型參數(shù)的值能使用哪些類型长酗。受限通配符描述幾乎不知道是什么類型的未知類型的層次結(jié)構(gòu)溪北,其實(shí)想表達(dá)的是這種意思:“我不知道到底是什么類型,但我知道這種類型實(shí)現(xiàn)了 List 接口夺脾≈Γ”在類型參數(shù)中,這句話表達(dá)的意思可以寫成 ? extends List劳翰。這為程序員提供了一線希望敦锌,至少知道可以使用的類型要滿足什么條件,而不是對(duì)類型一無(wú)所知佳簸。

不管限定使用的類型是類還是接口乙墙,都要使用 extends 關(guān)鍵字。這是類型變體(type variance)的一個(gè)示例生均。類型變體是容器類型之間的繼承關(guān)系和負(fù)載類型的繼承關(guān)系有所關(guān)聯(lián)的理論基礎(chǔ)听想。

? 類型協(xié)變

這表示容器類型之間和負(fù)載類型之間具有相同的關(guān)系。這種關(guān)系通過 extends 關(guān)鍵字表示马胧。

? 類型逆變

這表示容器類型之間和負(fù)載類型之間具有相反的關(guān)系汉买。這種關(guān)系通過 super 關(guān)鍵字表示。

容器類型作為類型的制造者或使用者時(shí)會(huì)體現(xiàn)這些原則佩脊。例如蛙粘,如果 Cat 類擴(kuò)展 Pet 類,那么 List<Cat> 是 List<? extends Pet> 的子類型威彰。這里出牧,List 是 Cat 對(duì)象的制造者,應(yīng)該使用關(guān)鍵字 extends歇盼。

如果容器類型只是某種類型實(shí)例的使用者舔痕,就應(yīng)該使用 super 關(guān)鍵字。

Joshua Bloch 把這種用法總結(jié)成“Producer Extends, Consumer Super”原則(簡(jiǎn)稱 PECS,“制造者使用 extends伯复,使用者使用 super”)慨代。

Java 集合庫(kù)大量使用了協(xié)變和逆變。大量使用這兩種變體的目的是確保泛型“做正確的事”啸如,以及表現(xiàn)出的行為不會(huì)讓開發(fā)者詫異侍匙。

2. 數(shù)組協(xié)變

在早期的 Java 版本中,集合庫(kù)還沒有出現(xiàn)组底,容器類型的類型變體問題在 Java 的數(shù)組中也有體現(xiàn)丈积。沒有類型變體,即使 sort() 這樣簡(jiǎn)單的方法也很難使用有效的方式編寫:

基于這個(gè)原因债鸡,Java 的數(shù)組可以協(xié)變——盡管這么做讓靜態(tài)類型系統(tǒng)暴露出了缺陷江滨,但在Java 平臺(tái)的早期階段仍是必要之惡:

最近對(duì)現(xiàn)代開源項(xiàng)目的研究表明,數(shù)組協(xié)變極少使用厌均,幾乎可以斷定為編程語(yǔ)言的設(shè)計(jì)缺陷唬滑。因此,編寫新代碼時(shí)棺弊,應(yīng)該避免使用數(shù)組協(xié)變晶密。

3. 泛型方法

泛型方法是參數(shù)可以使用任何引用類型實(shí)例的方法。例如模她,下述方法模擬 C 語(yǔ)言中 ,(逗號(hào))運(yùn)算符的功能稻艰。這個(gè)運(yùn)算符一般用來(lái)合并有副作用的表達(dá)式。

雖然這個(gè)方法的定義中使用了類型參數(shù)侈净,但所在的類不需要定義為泛型類尊勿。使用這種句法是為了表明這個(gè)方法可以自由使用,而且返回類型和參數(shù)的類型一樣畜侦。

4. 使用和設(shè)計(jì)泛型

使用 Java 的泛型時(shí)元扔,有時(shí)要從兩方面思考問題

? 使用者要使用現(xiàn)有的泛型庫(kù),還要編寫一些相對(duì)簡(jiǎn)單的泛型類旋膳。對(duì)使用者來(lái)說澎语,要理解類型擦除的基本知識(shí),因?yàn)槿绻恢肋\(yùn)行時(shí)對(duì)泛型的處理方式验懊,會(huì)對(duì)幾個(gè) Java 句法感到困惑擅羞。

? 設(shè)計(jì)者使用泛型開發(fā)新庫(kù)時(shí),設(shè)計(jì)者需要理解泛型的更多功能义图。規(guī)范中有一些難以理解的部分祟滴,例如要完全理解通配符和“capture-of”錯(cuò)誤消息等高級(jí)話題。

泛型是 Java 語(yǔ)言規(guī)范中最難理解的部分之一歌溉,潛藏很多極端情況,并不需要每個(gè)開發(fā)者都完全理解,至少初次接觸 Java 的類型系統(tǒng)時(shí)沒必要痛垛。


編譯時(shí)和運(yùn)行時(shí)類型

假設(shè)有如下的代碼片段:

我們可以問這個(gè)問題:l 是什么類型?答案取決于在編譯時(shí)(即 javac 看到的類型)還是運(yùn)行時(shí)(JVM 看到的類型)問這個(gè)問題草慧。

javac 把 l 看成 List-of-String 類型,而且會(huì)用這個(gè)類型信息仔細(xì)檢查句法錯(cuò)誤匙头,例如不能使用 add() 方法添加不合法的類型漫谷。

而 JVM 把 l 看成 ArrayList 類型的對(duì)象,這一點(diǎn)可以從 println() 語(yǔ)句的輸出中證實(shí)蹂析。因?yàn)橐脸愋吞蚴荆赃\(yùn)行時(shí) l 是原始類型。

因此电抚,編譯時(shí)和運(yùn)行時(shí)的類型稍微有些不同惕稻。某種程度上,這個(gè)不同點(diǎn)是蝙叛,運(yùn)行時(shí)類型既比編譯時(shí)類型精確俺祠,又沒有編譯時(shí)類型精確。

運(yùn)行時(shí)類型沒有編譯時(shí)類型精確借帘,因?yàn)闆]有負(fù)載類型的信息——這個(gè)信息被擦除了蜘渣,得到的運(yùn)行時(shí)類型只是原始類型。

編譯時(shí)類型沒有運(yùn)行時(shí)類型精確肺然,因?yàn)槲覀儾恢?l 的具體類型到底是什么蔫缸,只知道是一種和 List 兼容的類型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末际起,一起剝皮案震驚了整個(gè)濱河市拾碌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌加叁,老刑警劉巖倦沧,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異它匕,居然都是意外死亡展融,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門豫柬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)告希,“玉大人,你說我怎么就攤上這事烧给⊙嗯迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵础嫡,是天一觀的道長(zhǎng)指么。 經(jīng)常有香客問我酝惧,道長(zhǎng),這世上最難降的妖魔是什么伯诬? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任晚唇,我火速辦了婚禮,結(jié)果婚禮上盗似,老公的妹妹穿的比我還像新娘哩陕。我一直安慰自己,他們只是感情好赫舒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布悍及。 她就那樣靜靜地躺著,像睡著了一般接癌。 火紅的嫁衣襯著肌膚如雪心赶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天扔涧,我揣著相機(jī)與錄音园担,去河邊找鬼。 笑死枯夜,一個(gè)胖子當(dāng)著我的面吹牛弯汰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播湖雹,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咏闪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了摔吏?” 一聲冷哼從身側(cè)響起鸽嫂,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎征讲,沒想到半個(gè)月后据某,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诗箍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年癣籽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滤祖。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡筷狼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匠童,到底是詐尸還是另有隱情埂材,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布汤求,位于F島的核電站俏险,受9級(jí)特大地震影響严拒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜竖独,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一糙俗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧预鬓,春花似錦、人聲如沸赊颠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)竣蹦。三九已至顶猜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痘括,已是汗流浹背长窄。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纲菌,地道東北人挠日。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像翰舌,于是被迫代替她去往敵國(guó)和親嚣潜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 泛型椅贱,一個(gè)孤獨(dú)的守門者懂算。 大家可能會(huì)有疑問,我為什么叫做泛型是一個(gè)守門者庇麦。這其實(shí)是我個(gè)人的看法而已计技,我的意思是說泛...
    傳奇內(nèi)服號(hào)閱讀 404評(píng)論 0 0
  • 泛型是Java 1.5引入的新特性。泛型的本質(zhì)是參數(shù)化類型山橄,這種參數(shù)類型可以用在類垮媒、變量、接口和方法的創(chuàng)建中驾胆,分別...
    何時(shí)不晚閱讀 3,035評(píng)論 0 2
  • 我相信在大學(xué)里你總會(huì)認(rèn)識(shí)一個(gè)像這樣的姑娘——看起來(lái)文文弱弱涣澡,毫無(wú)心機(jī),但是你會(huì)發(fā)現(xiàn)她的男朋友恨不得比你換衣服還要快...
    木木家的木木閱讀 694評(píng)論 2 11
  • 今天去春游了丧诺。 陽(yáng)光正好入桂,微風(fēng)不燥。很是開心的一天驳阎,早晨八點(diǎn)出發(fā)抗愁,一路上風(fēng)景很美馁蒂,心情激動(dòng)無(wú)比。在這樣的季...
    尖娃閱讀 515評(píng)論 4 12
  • 人生感悟,與同是生命過客的你我分享撮珠。
    方露心閱讀 169評(píng)論 0 1