泛型最常用于集合,如Set<E>和Map<K,V>,以及單個元素的容器,如ThreadLocal<T>和AtomicReference<T>。在所有這些用法中,它都充當被參數化了的容器。這樣就限制每個容器只能有固定數目的類型參數。一般來說,這種情況正是你想要的。一個Set只有一個參數類型,表示它的元素類型;一個Map有兩個類型參數,表示它的鍵和值類型。
但是,有時候你會需要更多的靈活性。例如,數據庫的行可以有任意數量的列,如果能以類型安全的方式訪問所有列就好了。幸運的是,有一種方法可以很容易的做到這一點。這種方法就是將鍵(key)進行參數化而不是將容器參數化。然后將參數化的鍵提交給容器來插入或者獲取值。用泛型系統來確保值得類型與它的鍵相符。
下面簡單的示范一下這種方法:以Favorites類為例,它允許其客戶端從任意數量的其他類中,保存并獲取一個“最喜愛“的實例。Class對象充當參數化鍵的部分。之所以可以這樣,是因為Class被泛型化了。類的類型從字面上看起來不再只是簡單的Class,而是Class<T>。例如,Stirng.class屬于Class<String>類型,Integer.class屬于Class<Integer>類型。當一個類的字面被用在方法中,來傳達編譯時和運行時的類型信息時,就被稱作類型令牌(type token)。
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
下面是一個示例程序,檢驗一下Favorites類,它將保存、獲取并打印一個最喜愛的String、Integer和Class實例:
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
正如所料,這段程序打印出的是Java cafebabe Favorites。注意,有時Java的printf方法與C語言中的不同,C語言中使用\n的地方,在Java中應該使用%n,這個%n會產生適用特定平臺的行分隔符,在許多平臺上是\n,但是并非所有平臺都是如此。
Favorites實例是類型安全(typesafe)的:當你向它請求String的時候,它從來不會返回一個Integer給你。同樣它也是異構的(heterogeneous):不像普通的映射,它的所有鍵都是不同類型的。因此,我們將Favorites稱作類型安全的異構容器(typedafe heterogeneous container)。
Favorites的實現小得出奇。它的完整實現如下:
// Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
這里發生了一些微妙的事情。每個Favorites實例都得到一個稱作favorites的私有Map<Class<?>,Object>的支持。你可能認為由于無限制通配符類型的關系,將不能把任何東西放進這個Map中,但事實正好相反。要注意的是通配符類是嵌套的:它不是屬于通配符類型的Map的類型,而是它的鍵的類型。由此可見,每個鍵都可以有一個不同的參數化類型:一個可以是Class<String>,接下來是Class<Integer>等。異構就是從這里來的。
第二件要注意的事情是,favorites Map的值類型只是Object。換句話說,Map并不能保證鍵和值之間的類型關系,即不能保證每個值都為它的鍵所表示的類型(通俗的說,就是指鍵與值得類型并不相同)。事實上,Java的類型系統還沒有強大到足以表達這一點。但是我們知道這是事實,并在獲取faovrite的時候利用了這一點。
putFavorite方法的實現很簡單:它只是把(從指定的Class對象到指定的favorite實例)一個映射放到favorite中。如前所述,這是放棄了鍵和值之間的”類型聯系“,因此無法知道這個值是鍵的一個實例。但是沒關系,因為getFavorites方法能夠并且的確重新建立了這種聯系。
getFavorite方法的實現比putFavorite更難一些。它先從favorites映射中獲得與指定Class對象相對應的值。這正是要返回的對象引用,但它的編譯時類型是錯誤的。它的類型只是Object(favorites映射的值類型),我們需要返回一個T。因此,getFavorite方法的實現利用Class的cast方法,將對象引用動態的轉換成了對象所表示的類型。
cast方法是Java的轉換操作符的動態模擬。它只檢驗它的參數是否為Class對象所表示的類型的實例。如果是,就返回參數;否則就拋出ClassCastException異常。我們知道getFavorie中的cast永遠不會拋出ClassCastException異常,并假設客戶端代碼正確無誤的進行了編譯。也就是說,我們知道favorites映射中的值會始終與鍵的類型相匹配。
假設cast方法只返回它的參數,那它能為我們做什么呢?cast方法的簽名充分利用了Class類被泛型化的這個事實。它的返回類型是Class對象的類型參數。
public class Class<T> {
T cast(Object obj);
}
這正是getFavorite方法所需要的,也正是讓我們不必借助于未受檢的轉換成T就能確保Favorites類型安全的東西。
Favorites類有兩種局限性值得注意。首先,惡意的客戶端可以很輕松的破壞Favories實例的類型安全,只要它以原生態形式(raw form)適用Class對象。但是會造成客戶端代碼在編譯時產生未受檢的警告。這與一般的集合實現,如HashSet和HashMap并沒有什么區別。你可以很容易的利用原生態類型HashSet(詳見第26條)將String放進HashSet<Integer>中。也就是說,如果愿意付出一點點代價,就可以擁有運行時的類型安全。確保Favorites永遠不違背它的類型約束條件的方式是,讓putFavorite方法檢驗instance是否真的是type所表示的類型的實例。只需使用一個動態的轉換,如下代碼所示:
// Achieving runtime type safety with a dynamic cast
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
java.util.Collections中有一些集合包裝類采用了同樣的技巧。它們稱作checkedSet、checkedList、checkedMap,諸如此類。除了一個集合(或者映射)之外,它們的靜態工廠還采用一個(或者兩個)Class對象。靜態工廠屬于泛型方法。確保Class對象和集合的編譯時類型相匹配。包裝類給它們所封裝的集合增加了具體化。例如,如果有人視圖將Coin放進你的Collection<Stamp>,包裝類就會在運行時拋出ClassCastException異常。用這些包裝類在混有泛型和原生態類型的應用程序中追溯“是誰把錯誤的類型元素添加到了集合中”很有幫助。
Favorites類的第二種局限性在于它不能用在不可具體化的(non-reifiable)類型中(詳見第28條)。換句話說,你可以保存最喜愛的String或者String[],但是不能保存最喜愛的List<String>。你可以保存最喜愛的String或者String[],但不能保存最喜愛的List<String>。換句話說,如果試圖保存最喜愛的List<String>,程序就不能進行編譯。原因在于你無法為List<String>獲得一個Class對象:List<String>.class是個語法錯誤,這是件好事。List<String>和List<Integer>共用一個Class對象,即List.class。如果從“類型安全的字面”上來看,List<String>.class和List<Integer>.class是合法的,并返回了相同的對象引用,這就破壞了Favorites對象的內部結構。對于這種局限性,還沒有完全令人滿意的解決辦法。
Favorites使用的類型令牌(type token)是無限制的:getFavorite和putFavtite接受任何Class對象。有時可能需要限制那些可以傳給方法的類型。這可以通過有限制的類型令牌來實現,它只要一個類型令牌,利用有限制類型參數(詳見第30條)或者有限制通配符來限制可以表示的類型。
注解API(詳見第39條)廣泛利用了有限制的類型令牌。例如,這是一個在運行時讀取注解的方法。這個方法來自AnnotatedElement接口,它通過表示類、方法、域及其它程序元素的反射類型來實現:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
參數annotionType是一個表示注解類型的有限制的類型令牌。如果元素有這種類型的注解,該方法就將它返回;如果沒有,則返回null。被注解的元素本質上是個類型安全的異構容器,容器的鍵屬于注解類型。
假設你有一個類型為Class<?>的對象,并且想將它傳給一個需要有限制的類型令牌的方法,例如getAnnotation。你可以將對象轉換成Class<? extends Annotation>,但是這種轉換是非受檢的,因此會產生一條編譯時警告(詳見第27條)。幸運的是,類Class提供了一個安全(且動態)的執行這種轉換的實例方法。該方法稱作asSubclass,它將調用它的Class對象轉換成其參數表示的類的一個子類。如果轉換成功,該方法返回它的參數,如果失敗,則拋出ClassCastException異常。
下面示范如何利用asSubclass方法在編譯時讀取類型未知的注解。這個方法編譯時沒有出現錯誤或者警告。
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
總而言之,集合API說明了泛型的一般用法,限制每個容器只能有固定數目的類型參數。你可以通過將類型參數放在鍵上而不是容器上來避開這一限制。對于這種類型安全的異構容器,可以用Class對象作為鍵。以這種方式使用的Class對象稱作類型令牌。你也可以使用定制的鍵類型。例如,用一個DatabaseRow類型表示一個數據庫行(容器),用泛型Column<T>作為它的鍵。