將泛型添加到 Java 語言中增加了類型系統的複雜性,提高了許多變數和方法聲明的冗長程度。因為沒有提供 “typedef” 工具來定義類型的簡簡短名稱,所以有些開發人員轉而把擴充當作 “窮人的 typedef”,結果收到了良好的效果。
對於 Java 5.0 中新增的泛型工具,一個常見的抱怨就是,它使代碼變得太冗長。原來用一行就夠的變數聲明不再存在了,與聲明參數化型別有關的重複非常討厭,特別是還沒有良好地支援自動補足的 IDE。例如,如果想聲明一個 Map,它的鍵是 Socket,值是 Future<String>,那麼老方法就是:
Map socketOwner = new HashMap();
比新方法緊湊得多:
Map<Socket, Future<String>> socketOwner = new HashMap<Socket, Future<String>>();
當然,新方法內建了更多類型資訊,減少了編程錯誤,提高了程式的可讀性,但是確實帶來了更多聲明變數和方法簽名方面的前期工作。型別參數在聲明和初始化中的重複看起來尤其沒有必要;Socket 和 Future<String> 需要輸入兩次,這迫使我們違犯了 “DRY” 原則(不要重複自己)。
合成類似於 typedef 的東西
添加泛型給類型系統增加了一些複雜性。在 Java 5.0 之前,“type” 和 “class” 幾乎是同義的,而參數化型別,特別是那些綁定的通配類型,使子類型和子類的概念有了顯著區別。類型 ArrayList<?>、ArrayList<? extends Number> 和 ArrayList<Integer> 是不同的類型,雖然它們是由同一個類 ArrayList 實現的。這些類型構成了一個階層;ArrayList<?> 是 ArrayList<? extends Number> 的超類型,而 ArrayList<? extends Number> 是 ArrayList<Integer> 的超類型。
對於原來的簡單類型系統,像 C 的 typedef 這樣的特性沒有意義。但是對於更複雜的類型系統,typedef 工具可能會提供一些好處。不知是好還是壞,總之在泛型加入的時候,typedef 沒有加入 Java 語言。
有些人用作 “窮人的 typedef” 的一個(壞的)做法是一個小小的擴充:建立一個類,擴充泛型型別,但是不添加功能,例如 SocketUserMap 類型,如清單 1 所示:
清單 1. 偽 typedef 反模式 —— 不要這麼做
public class SocketUserMap extends HashMap<Socket<Future<String>> { } SocketUserMap socketOwner = new SocketUserMap();
我將這個技巧稱為
偽 typedef 反模式,它實現了將 socketOwner 定義簡化為一行的這一(有問題的)目標,但是有些副作用,最終成為重用和維護的障礙。(對於有明確的建構函式而不是無參建構函式的類來說,衍生類別也需要聲明每個建構函式,因為建構函式沒有被繼承。)
偽類型的問題
在 C 中,用 typedef 定義一個新類型更像是宏,而不是型別宣告。定義等價類別型的 typedef,可以與原始類型自由地互換。清單 2 顯示了一個定義回呼函數的樣本,其中在簽名中使用了一個 typedef,但是調用者提供給回調的是一個等價類別型,而編譯器和運行時都可以接受它:
清單 2. C 語言的 typedef 樣本
// Define a type called "callback" that is a function pointer typedef void (*Callback)(int); void doSomething(Callback callback) { } // This function conforms to the type defined by Callback void callbackFunction(int arg) { } // So a caller can pass the address of callbackFunction to doSomething void useCallback() { doSomething(&callbackFunction); }
擴充不是類型定義
用 Java 語言編寫的試圖使用偽 typedef 的等價程式就會出現麻煩。清單 3 的 StringList 和 UserList 類型都擴充了一個公用超類,但是它們不是等價的類型。這意味著任何想調用 lookupAll 的代碼都必須傳遞一個 StringList,而不能是 List<String> 或 UserList。
清單 3. 偽類型如何把客戶限定在只能使用偽類型
class StringList extends ArrayList<String> { } class UserList extends ArrayList<String> { } ... class SomeClass { public void validateUsers(UserList users) { ... } public UserList lookupAll(StringList names) { ... } }
這個限制要比初看上去嚴格得多。在小程式中,可能不會有太大差異,但是當程式變大的時候,使用偽類型的需求就會不斷地造成問題。如果變數類型是 StringList,就不能給它分配普通的 List<String>,因為 List<String> 是 StringList 的超類型,所以不是 StringList。就像不能把 Object 分配給類型為 String 的變數一樣,也不能把 List<String> 分配給類型為 StringList 的變數(但是,可以反過來,例如,可以把 StringList 分配給類型為 List<String> 的變數,因為 List<String> 是 StringList 的超類型。)
同樣的情況也適用於方法的參數;如果一個方法參數是 StringList 類型,那麼就不能把普通的 List<String> 傳遞給它。這意味著,如果不要求這個方法的每次使用都使用偽類型,那麼根本不能用偽類型作為方法參數,而這在實踐當中就意味著在庫 API 中根本就不能使用偽類型。而且大多數庫 API 都源自本來沒想成為庫代碼的那些代碼,所以 “這個代碼只是給我自己的,沒有其他人會用它” 可不是個好借口(只要您的代碼有一點兒用處,別人就有可能會使用它;如果您的代碼臭得很,那您可能是對的)。
偽類型會傳染
這種 “病毒” 性質是讓 C 代碼的重用有困難的因素之一。差不多每個 C 包都有標頭檔,定義工具宏和類型,像 int32、boolean、true、false,諸如此類。如果想在一個應用程式內使用幾個包,而它們對於這些公用條目沒有使用相同的定義,那麼即使要編譯一個只包含所有標頭檔的空程式,之前也要在 “標頭檔地獄” 問題上花好長時間。如果編寫的 C 應用程式要使用許多來自不同作者的不同的包,那麼幾乎肯定要涉及一些這類痛苦。另一方面,對於 Java 應用程式來說,在沒有這類痛苦的情況下使用許多甚至更多的包,是非常常見的事。如果包要在它們的 API 中使用偽類型,那麼我們可能就要重新經曆早已留在痛苦回憶中的問題。
作為樣本,假設有兩個不同的包,每個包都用偽類型反模式定義了 StringList,如清單 4 所示,而且每個包都定義了操作 StringList 的工具方法。兩個包都定義了同樣的標識符,這一事實已經是不方便的一個小源頭了;客戶程式必須選擇匯入一個定義,而另一個定義則要使用完整名稱。但是更大的問題是現在這些包的客戶無法建立既能傳遞給 sortList 又能傳遞給 reverseList 的對象,因為兩個不同的 StringList 類型是不同的類型,彼此互不相容。客戶現在必須在使用一個包還是使用另一個包之間進行選擇,否則他們就必須做許多工作,在不同類型的 StringList 之間進行轉換。對包的作者來說以為方便的東西,成為在所有地方使用這個包的突出障礙,除非在最受限的環境中。
清單 4. 偽類型的使用如何妨礙重用
package a; class StringList extends ArrayList<String> { } class ListUtilities { public static void sortList(StringList list) { } } package b; class StringList extends ArrayList<String> { } class SomeOtherUtilityClass { public static void reverseList(StringList list) { } } ... class Client { public void someMethod() { StringList list = ...; // Can't do this ListUtilities.sortList(list); SomeOtherUtilityClass.reverseList(list); } }
偽類型通常太具體
偽類型反模式的進一步問題是,它會喪失使用介面定義變數類型和方法參數的好處。雖然可以把 StringList 定義成擴充 List<String> 的介面,再定義一個具體類型 StringArrayList 來擴充 ArrayList<String> 並實現 StringList,但多數偽 typedef 反模式的使用者通常達不到這種水平,因為這項技術的目的主要是為了簡化和縮短類型的名稱。但結果是,API 的用處減少了並變得更脆弱,因為它們使用 ArrayList 這樣的具體類型,而不是 List 這樣的抽象類別型。
更安全的技巧
一個更安全的減少聲明泛型集合所需打字量的技巧是使用
類型推導(type inference)。編譯器可以非常聰明地使用程式中內嵌的類型資訊來配置類型參數。如果定義了下面這樣一個工具方法:
public static <K,V> Map<K,V> newHashMap() { return new HashMap<K,V>(); }
那麼可以安全地用它來避免錄入兩次參數:
Map<Socket, Future<String>> socketOwner = Util.newHashMap();
這種方法之所以能夠奏效,在於編譯器可以根據泛型方法 newHashMap() 被調用的位置推匯出 K 和 V 的值。
結束語
偽 typedef 反模式的動機很簡單 —— 開發人員想要一種方法可以定義更緊湊的類型標識符,特別是在泛型把類型標識符變得更冗長的時候。問題在於這個做法在使用它的代碼和代碼的客戶之間形成了緊密的耦合,從而妨礙了重用。不喜歡泛型型別標識符的冗長是可以理解的,但這不是解決問題的辦法。