Java 理論與實(shí)踐: 偽 typedef 反模式
將泛型添加到 Java? 語(yǔ)言中增加了類(lèi)型系統(tǒng)的復(fù)雜性,提高了許多變量和方法聲明的冗長(zhǎng)程度。因?yàn)闆](méi)有提供 “typedef” 工具來(lái)定義類(lèi)型的簡(jiǎn)短名稱(chēng),所以有些開(kāi)發(fā)人員轉(zhuǎn)而把擴(kuò)展當(dāng)作 “窮人的 typedef”,但是收到的決不是好的結(jié)果。在這個(gè)月的 Java 理論與實(shí)踐 中,Java 專(zhuān)家 Brian Goetz 解釋了這個(gè) “反模式” 的限制。
對(duì)于 Java 5.0 中新增的泛型工具,一個(gè)常見(jiàn)的抱怨就是,它使代碼變得太冗長(zhǎng)。原來(lái)用一行就夠的變量聲明不再存在了,與聲明參數(shù)化類(lèi)型有關(guān)的重復(fù)非常討厭,特別是還沒(méi)有良好地支持自動(dòng)補(bǔ)足的 IDE。例如,如果想聲明一個(gè) Map
,它的鍵是 Socket
,值是 Future<String>
,那么老方法就是:
|
比新方法緊湊得多:
|
當(dāng)然,新方法內(nèi)置了更多類(lèi)型信息,減少了編程錯(cuò)誤,提高了程序的可讀性,但是確實(shí)帶來(lái)了更多聲明變量和方法簽名方面的前期工作。類(lèi)型參數(shù)在聲明和初始化中的重復(fù)看起來(lái)尤其沒(méi)有必要;Socket
和 Future<String>
需要輸入兩次,這迫使我們違犯了 “DRY” 原則(不要重復(fù)自己)。
添加泛型給類(lèi)型系統(tǒng)增加了一些復(fù)雜性。在 Java 5.0 之前,“type” 和 “class” 幾乎是同義的,而參數(shù)化類(lèi)型,特別是那些綁定的通配類(lèi)型,使子類(lèi)型和子類(lèi)的概念有了顯著區(qū)別。類(lèi)型 ArrayList<?>
、ArrayList<? extends Number>
和 ArrayList<Integer>
是不同的類(lèi)型,雖然它們是由同一個(gè)類(lèi) ArrayList
實(shí)現(xiàn)的。這些類(lèi)型構(gòu)成了一個(gè)層次結(jié)構(gòu);ArrayList<?>
是 ArrayList<? extends Number>
的超類(lèi)型,而 ArrayList<? extends Number>
是 ArrayList<Integer>
的超類(lèi)型。
對(duì)于原來(lái)的簡(jiǎn)單類(lèi)型系統(tǒng),像 C 的 typedef 這樣的特性沒(méi)有意義。但是對(duì)于更復(fù)雜的類(lèi)型系統(tǒng),typedef 工具可能會(huì)提供一些好處。不知是好還是壞,總之在泛型加入的時(shí)候,typedef 沒(méi)有加入 Java 語(yǔ)言。
有些人用作 “窮人的 typedef” 的一個(gè)(壞的)做法是一個(gè)小小的擴(kuò)展:創(chuàng)建一個(gè)類(lèi),擴(kuò)展泛型類(lèi)型,但是不添加功能,例如 SocketUserMap
類(lèi)型,如清單 1 所示:
清單 1. 偽 typedef 反模式 —— 不要這么做
|
我將這個(gè)技巧稱(chēng)為偽 typedef 反模式,它實(shí)現(xiàn)了將 socketOwner
定義簡(jiǎn)化為一行的這一(有問(wèn)題的)目標(biāo),但是有些副作用,最終成為重用和維護(hù)的障礙。(對(duì)于有明確的構(gòu)造函數(shù)而不是無(wú)參構(gòu)造函數(shù)的類(lèi)來(lái)說(shuō),派生類(lèi)也需要聲明每個(gè)構(gòu)造函數(shù),因?yàn)闃?gòu)造函數(shù)沒(méi)有被繼承。)
![]() ![]() |
![]()
|
在 C 中,用 typedef
定義一個(gè)新類(lèi)型更像是宏,而不是類(lèi)型聲明。定義等價(jià)類(lèi)型的 typedef,可以與原始類(lèi)型自由地互換。清單 2 顯示了一個(gè)定義回調(diào)函數(shù)的示例,其中在簽名中使用了一個(gè) typedef,但是調(diào)用者提供給回調(diào)的是一個(gè)等價(jià)類(lèi)型,而編譯器和運(yùn)行時(shí)都可以接受它:
清單 2. C 語(yǔ)言的 typedef 示例
|
用 Java 語(yǔ)言編寫(xiě)的試圖使用偽 typedef 的等價(jià)程序就會(huì)出現(xiàn)麻煩。清單 3 的 StringList
和 UserList
類(lèi)型都擴(kuò)展了一個(gè)公共超類(lèi),但是它們不是等價(jià)的類(lèi)型。這意味著任何想調(diào)用 lookupAll
的代碼都必須傳遞一個(gè) StringList
,而不能是 List<String>
或 UserList
。
清單 3. 偽類(lèi)型如何把客戶(hù)限定在只能使用偽類(lèi)型
|
這個(gè)限制要比初看上去嚴(yán)格得多。在小程序中,可能不會(huì)有太大差異,但是當(dāng)程序變大的時(shí)候,使用偽類(lèi)型的需求就會(huì)不斷地造成問(wèn)題。如果變量類(lèi)型是 StringList
,就不能給它分配普通的 List<String>
,因?yàn)?List<String>
是 StringList
的超類(lèi)型,所以不是 StringList
。就像不能把 Object
分配給類(lèi)型為 String
的變量一樣,也不能把 List<String>
分配給類(lèi)型為 StringList
的變量(但是,可以反過(guò)來(lái),例如,可以把 StringList
分配給類(lèi)型為 List<String>
的變量,因?yàn)?List<String>
是 StringList
的超類(lèi)型。)
同樣的情況也適用于方法的參數(shù);如果一個(gè)方法參數(shù)是 StringList
類(lèi)型,那么就不能把普通的 List<String>
傳遞給它。這意味著,如果不要求這個(gè)方法的每次使用都使用偽類(lèi)型,那么根本不能用偽類(lèi)型作為方法參數(shù),而這在實(shí)踐當(dāng)中就意味著在庫(kù) API 中根本就不能使用偽類(lèi)型。而且大多數(shù)庫(kù) API 都源自本來(lái)沒(méi)想成為庫(kù)代碼的那些代碼,所以 “這個(gè)代碼只是給我自己的,沒(méi)有其他人會(huì)用它” 可不是個(gè)好借口(只要您的代碼有一點(diǎn)兒用處,別人就有可能會(huì)使用它;如果您的代碼臭得很,那您可能是對(duì)的)。
這種 “病毒” 性質(zhì)是讓 C 代碼的重用有困難的因素之一。差不多每個(gè) C 包都有頭文件,定義工具宏和類(lèi)型,像 int32
、boolean
、true
、false
,諸如此類(lèi)。如果想在一個(gè)應(yīng)用程序內(nèi)使用幾個(gè)包,而它們對(duì)于這些公共條目沒(méi)有使用相同的定義,那么即使要編譯一個(gè)只包含所有頭文件的空程序,之前也要在 “頭文件地獄” 問(wèn)題上花好長(zhǎng)時(shí)間。如果編寫(xiě)的 C 應(yīng)用程序要使用許多來(lái)自不同作者的不同的包,那么幾乎肯定要涉及一些這類(lèi)痛苦。另一方面,對(duì)于 Java 應(yīng)用程序來(lái)說(shuō),在沒(méi)有這類(lèi)痛苦的情況下使用許多甚至更多的包,是非常常見(jiàn)的事。如果包要在它們的 API 中使用偽類(lèi)型,那么我們可能就要重新經(jīng)歷早已留在痛苦回憶中的問(wèn)題。
作為示例,假設(shè)有兩個(gè)不同的包,每個(gè)包都用偽類(lèi)型反模式定義了 StringList
,如清單 4 所示,而且每個(gè)包都定義了操作 StringList
的工具方法。兩個(gè)包都定義了同樣的標(biāo)識(shí)符,這一事實(shí)已經(jīng)是不方便的一個(gè)小源頭了;客戶(hù)程序必須選擇導(dǎo)入一個(gè)定義,而另一個(gè)定義則要使用完全限定的名稱(chēng)。但是更大的問(wèn)題是現(xiàn)在這些包的客戶(hù)無(wú)法創(chuàng)建既能傳遞給 sortList
又能傳遞給 reverseList
的對(duì)象,因?yàn)閮蓚€(gè)不同的 StringList
類(lèi)型是不同的類(lèi)型,彼此互不兼容。客戶(hù)現(xiàn)在必須在使用一個(gè)包還是使用另一個(gè)包之間進(jìn)行選擇,否則他們就必須做許多工作,在不同類(lèi)型的 StringList
之間進(jìn)行轉(zhuǎn)換。對(duì)包的作者來(lái)說(shuō)以為方便的東西,成為在所有地方使用這個(gè)包的突出障礙,除非在最受限的環(huán)境中。
清單 4. 偽類(lèi)型的使用如何妨礙重用
|
偽類(lèi)型反模式的進(jìn)一步問(wèn)題是,它會(huì)喪失使用接口定義變量類(lèi)型和方法參數(shù)的好處。雖然可以把 StringList
定義成擴(kuò)展 List<String>
的接口,再定義一個(gè)具體類(lèi)型 StringArrayList
來(lái)擴(kuò)展 ArrayList<String>
并實(shí)現(xiàn) StringList
,但多數(shù)偽 typedef 反模式的用戶(hù)通常達(dá)不到這種水平,因?yàn)檫@項(xiàng)技術(shù)的目的主要是為了簡(jiǎn)化和縮短類(lèi)型的名稱(chēng)。但結(jié)果是,API 的用處減少了并變得更脆弱,因?yàn)樗鼈兪褂?ArrayList
這樣的具體類(lèi)型,而不是 List
這樣的抽象類(lèi)型。
一個(gè)更安全的減少聲明泛型集合所需打字量的技巧是使用類(lèi)型推導(dǎo)(type inference)。編譯器可以非常聰明地使用程序中內(nèi)嵌的類(lèi)型信息來(lái)分配類(lèi)型參數(shù)。如果定義了下面這樣一個(gè)工具方法:
|
那么可以安全地用它來(lái)避免錄入兩次參數(shù):
|
這種方法之所以能夠奏效,在于編譯器可以根據(jù)泛型方法
newHashMap()
被調(diào)用的位置推導(dǎo)出 K
和 V
的值。 ![]() ![]() |
![]()
|
偽 typedef 反模式的動(dòng)機(jī)很簡(jiǎn)單 —— 開(kāi)發(fā)人員想要一種方法可以定義更緊湊的類(lèi)型標(biāo)識(shí)符,特別是在泛型把類(lèi)型標(biāo)識(shí)符變得更冗長(zhǎng)的時(shí)候。問(wèn)題在于這個(gè)做法在使用它的代碼和代碼的客戶(hù)之間形成了緊密的耦合,從而妨礙了重用。不喜歡泛型類(lèi)型標(biāo)識(shí)符的冗長(zhǎng)是可以理解的,但這不是解決問(wèn)題的辦法。
posted on 2006-08-24 17:34 Binary 閱讀(157) 評(píng)論(0) 編輯 收藏