第三十三條:優先考慮類型安全的異構造器【泛型end】

泛型最常用于集合,如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>作為它的鍵。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370