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 兼容的類型。