Effective Java 第三版——33. 優先考慮類型安全的異構容器

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

33. 優先考慮類型安全的異構容器

泛型的常見用法包括集合,如Set <E>Map <K,V>和單個元素容器,如ThreadLocal <T>AtomicReference <T>。 在全部這些用途中,它都是參數化的容器。 這限制了每一個容器只能有固定數量的類型參數。 一般這正是你想要的。 一個Set有單一的類型參數,表示它的元素類型; 一個Map有兩個,表明它的鍵和值的類型;等等。數據庫

然而有時候,你須要更多的靈活性。 例如,數據庫一行記錄能夠具備任意多列,而且可以以類型安全的方式訪問它們是很好的。 幸運的是,有一個簡單的方法能夠達到這個效果。 這個想法是參數化鍵(key)而不是容器。 而後將參數化的鍵提交給容器以插入或檢索值。 泛型類型系統用於保證值的類型與其鍵一致。安全

做爲這種方法的一個簡單示例,請考慮一個Favorites類,它容許其客戶端保存和檢索任意多種類型的favorite實例。 該類型的Class對象將扮演參數化鍵的一部分。其緣由是這Class類是泛型的。 類的類型從字面上來講不是簡單的Class,而是Class <T>。 例如,String.class的類型爲Class <String>Integer.class的類型爲Class <Integer>。 當在方法中傳遞字面類傳遞編譯時和運行時類型信息時,它被稱爲類型令牌(type token)[Bracha04]。學習

Favorites類的API很簡單。 它看起來就像一個簡單Map類,除了該鍵是參數化的之外。 客戶端在設置和獲取favorites實例時呈現一個Class對象。 這裏是API:ui

// 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類,保存,檢索和打印喜歡的StringIntegerClass實例:翻譯

// 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語言的不一樣之處在於,應該使用%n,而在C中使用\n%n生成適用的特定於平臺的行分隔符,該分隔符在不少但不是全部平臺上都是\ncode

Favorites實例是類型安全的:當你請求一個字符串時它永遠不會返回一個整數。 它也是異構的:與普通Map不一樣,全部的鍵都是不一樣的類型。 所以,咱們將Favorites稱爲類型安全異構容器(typesafe heterogeneous container.)。對象

Favorites的實現很是小巧。 這是完整的代碼:blog

// 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>等等。 這就是異構的由來。token

接下來要注意的是,favorites的Map的值類型只是Object。 換句話說,Map不保證鍵和值之間的類型關係,即每一個值都是由其鍵表示的類型。 事實上,Java的類型系統並不足以表達這一點。 可是咱們知道這是真的,並在檢索一個favorite時利用了這點。

putFavorite實現很簡單:只需將給定的Class對象映射到給定的favorites的實例便可。 如上所述,這丟棄了鍵和值之間的「類型聯繫(type linkage)」;沒法知道這個值是否是鍵的一個實例。 但不要緊,由於getFavorites方法能夠而且確實從新創建這種關聯。

getFavorite的實現比putFavorite更復雜。 首先,它從favorites Map中獲取與給定Class對象相對應的值。 這是返回的正確對象引用,但它具備錯誤的編譯時類型:它是Object(favorites map的值類型),咱們須要返回類型T。所以,getFavorite實現動態地將對象引用轉換爲Class對象表示的類型,使用Class的cast方法。

cast方法是Java的cast操做符的動態模擬。它只是檢查它的參數是否由Class對象表示的類型的實例。若是是,它返回參數;不然會拋出ClassCastException異常。咱們知道,假設客戶端代碼可以乾淨地編譯,getFavorite中的強制轉換不會拋出ClassCastException異常。 也就是說,favorites map中的值始終與其鍵的類型相匹配。

那麼這個cast方法爲咱們作了什麼,由於它只是返回它的參數? cast的簽名充分利用了Class類是泛型的事實。 它的返回類型是Class對象的類型參數:

public class Class<T> {
    T cast(Object obj);
}

這正是getFavorite方法所須要的。 這正是確保Favorites類型安全,而不用求助一個未經檢查的強制轉換的T類型。

Favorites類有兩個限制值得注意。 首先,惡意客戶能夠經過使用原始形式的Class對象,輕鬆破壞Favorites實例的類型安全。 但生成的客戶端代碼在編譯時會生成未經檢查的警告。 這與正常的集合實現(如HashSet和HashMap)沒有什麼不一樣。 經過使用原始類型HashSet(條目 26),能夠輕鬆地將字符串放入HashSet <Integer>中。 也就是說,若是你願意爲此付出一點代價,就能夠擁有運行時類型安全性。 確保Favorites永遠不違反類型不變的方法是,使putFavorite方法檢查該實例是否由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中有一些集合包裝類,能夠發揮相同的訣竅。 它們被稱爲checkedSetcheckedListcheckedMap等等。 他們的靜態工廠除了一個集合(或Map)以外還有一個Class對象(或兩個)。 靜態工廠是泛型方法,確保Class對象和集合的編譯時類型匹配。 包裝類爲它們包裝的集合添加了具體化。 例如,若是有人試圖將Coin放入你的Collection <Stamp>中,則包裝類在運行時會拋出ClassCastException。 這些包裝類對於追蹤在混合了泛型和原始類型的應用程序中添加不正確類型的元素到集合的客戶端代碼頗有用。

Favorites類的第二個限制是它不能用於不可具體化的(non-reifiable)類型(條目 28)。 換句話說,你能夠保存你最喜歡的StringString [],但不能保存List <String>。 若是你嘗試保存你最喜歡的List <String>,程序將不能編譯。 緣由是沒法獲取List <String>的Class對象。 List <String> .class是語法錯誤,也是一件好事。 List <String>List <Integer>共享一個Class對象,即List.class。 若是「字面類型(type literals)」List <String> .classList <Integer> .class合法並返回相同的對象引用,那麼它會對Favorites對象的內部形成嚴重破壞。 對於這種限制,沒有徹底使人滿意的解決方法。

Favorites使用的類型令牌( type tokens)是無限制的:getFavoriteputFavorite接受任何Class對象。 有時你可能須要限制可傳遞給方法的類型。 這能夠經過一個有限定的類型令牌來實現,該令牌只是一個類型令牌,它使用限定的類型參數(條目 30)或限定的通配符(條目 31)來放置能夠表示的類型的邊界。

註解API(條目 39)普遍使用限定類型的令牌。 例如,如下是在運行時讀取註解的方法。 此方法來自AnnotatedElement接口,該接口由表示類,方法,屬性和其餘程序元素的反射類型實現:

public <T extends Annotation>
    T getAnnotation(Class<T> annotationType);

參數annotationType是表示註解類型的限定類型令牌。 該方法返回該類型的元素的註解(若是它有一個);若是沒有,則返回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的一般用法(以集合API爲例)限制了每一個容器的固定數量的類型參數。 你能夠經過將類型參數放在鍵上而不是容器上來解決此限制。 可使用Class對象做爲此類型安全異構容器的鍵。 以這種方式使用的Class對象稱爲類型令牌。 也可使用自定義鍵類型。 例如,能夠有一個表示數據庫行(容器)的DatabaseRow類型和一個泛型類型Column <T>做爲其鍵。

相關文章
相關標籤/搜索