Effective Java 第三版——32.合理地結合泛型和可變參數

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

Effective Java, Third Edition

32. 合理地結合泛型和可變參數

在Java 5中,可變參數方法(條目 53)和泛型都被添加到平臺中,因此你可能但願它們可以正常交互; 可悲的是,他們並無。 可變參數的目的是容許客戶端將一個可變數量的參數傳遞給一個方法,但這是一個脆弱的抽象( leaky abstraction):當你調用一個可變參數方法時,會建立一個數組來保存可變參數;那個應該是實現細節的數組是可見的。 所以,當可變參數具備泛型或參數化類型時,會致使編譯器警告混淆。安全

回顧條目 28,非具體化( non-reifiable)的類型是其運行時表示比其編譯時表示具備更少信息的類型,而且幾乎全部泛型和參數化類型都是不可具體化的。 若是某個方法聲明其可變參數爲非具體化的類型,則編譯器將在該聲明上生成警告。 若是在推斷類型不可肯定的可變參數參數上調用該方法,那麼編譯器也會在調用中生成警告。 警告看起來像這樣:dom

warning: [unchecked] Possible heap pollution from
    parameterized vararg type List<String>

當參數化類型的變量引用不屬於該類型的對象時會發生堆污染(Heap pollution)[JLS,4.12.2]。 它會致使編譯器的自動生成的強制轉換失敗,違反了泛型類型系統的基本保證。工具

例如,請考慮如下方法,該方法是第127頁上的代碼片斷的一個不太明顯的變體:學習

// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList;             // Heap pollution
    String s = stringLists[0].get(0); // ClassCastException
}

此方法沒有可見的強制轉換,但在調用一個或多個參數時拋出ClassCastException異常。 它的最後一行有一個由編譯器生成的隱形轉換。 這種轉換失敗,代表類型安全性已經被破壞,而且將值保存在泛型可變參數數組參數中是不安全的測試

這個例子引起了一個有趣的問題:爲何聲明一個帶有泛型可變參數的方法是合法的,當明確建立一個泛型數組是非法的時候呢? 換句話說,爲何前面顯示的方法只生成一個警告,而127頁上的代碼片斷會生成一個錯誤? 答案是,具備泛型或參數化類型的可變參數參數的方法在實踐中可能很是有用,所以語言設計人員選擇忍受這種不一致。 事實上,Java類庫導出了幾個這樣的方法,包括Arrays.asList(T... a)Collections.addAll(Collection<? super T> c, T... elements)EnumSet.of(E first, E... rest)。 與前面顯示的危險方法不一樣,這些類庫方法是類型安全的。翻譯

在Java 7中,SafeVarargs註解已添加到平臺,以容許具備泛型可變參數的方法的做者自動禁止客戶端警告。 實質上,SafeVarargs註解構成了做者對類型安全的方法的承諾。 爲了交換這個承諾,編譯器贊成不要警告用戶調用可能不安全的方法。設計

除非它其實是安全的,不然注意不要使用@SafeVarargs註解標註一個方法。 那麼須要作些什麼來確保這一點呢? 回想一下,調用方法時會建立一個泛型數組,以容納可變參數。 若是方法沒有在數組中存儲任何東西(它會覆蓋參數)而且不容許對數組的引用進行轉義(這會使不受信任的代碼訪問數組),那麼它是安全的。 換句話說,若是可變參數數組僅用於從調用者向方法傳遞可變數量的參數——畢竟這是可變參數的目的——那麼該方法是安全的。3d

值得注意的是,你能夠違反類型安全性,即便不會在可變參數數組中存儲任何內容。 考慮下面的泛型可變參數方法,它返回一個包含參數的數組。 乍一看,它可能看起來像一個方便的小工具:rest

// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
    return args;
}

這個方法只是返回它的可變參數數組。 該方法可能看起來並不危險,但它是! 該數組的類型由傳遞給方法的參數的編譯時類型決定,編譯器可能沒有足夠的信息來作出正確的判斷。 因爲此方法返回其可變參數數組,它能夠將堆污染傳播到調用棧上。

爲了具體說明,請考慮下面的泛型方法,它接受三個類型T的參數,並返回一個包含兩個參數的數組,隨機選擇:

static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
      case 0: return toArray(a, b);
      case 1: return toArray(a, c);
      case 2: return toArray(b, c);
    }
    throw new AssertionError(); // Can't get here
}

這個方法自己不是危險的,除了調用具備泛型可變參數的toArray方法以外,不會產生警告。

編譯此方法時,編譯器會生成代碼以建立一個將兩個T實例傳遞給toArray的可變參數數組。 這段代碼分配了一個Object []類型的數組,它是保證保存這些實例的最具體的類型,而無論在調用位置傳遞給pickTwo的對象是什麼類型。 toArray方法只是簡單地將這個數組返回給pickTwo,而後pickTwo將它返回給調用者,因此pickTwo老是返回一個Object []類型的數組。

如今考慮這個測試pickTwmain方法:

public static void main(String[] args) {
    String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

這種方法沒有任何問題,所以它編譯時不會產生任何警告。 可是當運行它時,拋出一個ClassCastException異常,儘管不包含可見的轉換。 你沒有看到的是,編譯器已經生成了一個隱藏的強制轉換爲由pickTwo返回的值的String []類型,以便它能夠存儲在屬性中。 轉換失敗,由於Object []不是String []的子類型。 這種故障至關使人不安,由於它從實際致使堆污染(toArray)的方法中移除了兩個級別,而且在實際參數存儲在其中以後,可變參數數組未被修改。

這個例子是爲了讓人們認識到給另外一個方法訪問一個泛型的可變參數數組是不安全的,除了兩個例外:將數組傳遞給另外一個可變參數方法是安全的,這個方法是用@SafeVarargs正確標註的, 將數組傳遞給一個非可變參數的方法是安全的,該方法僅計算數組內容的一些方法。

這裏是安全使用泛型可變參數的典型示例。 此方法將任意數量的列表做爲參數,並按順序返回包含全部輸入列表元素的單個列表。 因爲該方法使用@SafeVarargs進行標註,所以在聲明或其調用站位置上不會生成任何警告:

// Safe method with a generic varargs parameter
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

決定什麼時候使用SafeVarargs註解的規則很簡單:在每種方法上使用@SafeVarargs,並使用泛型或參數化類型的可變參數,這樣用戶就不會因沒必要要的和使人困惑的編譯器警告而擔心。 這意味着你不該該寫危險或者toArray等不安全的可變參數方法。 每次編譯器警告你可能會受到來自你控制的方法中泛型可變參數的堆污染時,請檢查該方法是否安全。 提醒一下,在下列狀況下,泛型可變參數方法是安全的:
1.它不會在可變參數數組中存儲任何東西

2.它不會使數組(或克隆)對不可信代碼可見。 若是違反這些禁令中的任何一項,請修復。

請注意,SafeVarargs註解只對不能被重寫的方法是合法的,由於不可能保證每一個可能的重寫方法都是安全的。 在Java 8中,註解僅在靜態方法和final實例方法上合法; 在Java 9中,它在私有實例方法中也變爲合法。

使用SafeVarargs註解的替代方法是採用條目 28的建議,並用List參數替換可變參數(這是一個變相的數組)。 下面是應用於咱們的flatten方法時,這種方法的樣子。 請注意,只有參數聲明被更改了:

// List as a typesafe alternative to a generic varargs parameter
static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

而後能夠將此方法與靜態工廠方法List.of結合使用,以容許可變數量的參數。 請注意,這種方法依賴於List.of聲明使用@SafeVarargs註解:
audience = flatten(List.of(friends, romans, countrymen));

這種方法的優勢是編譯器能夠證實這種方法是類型安全的。 沒必要使用SafeVarargs註解來證實其安全性,也不用擔憂在肯定安全性時可能會犯錯。 主要缺點是客戶端代碼有點冗長,運行可能會慢一些。

這個技巧也能夠用在不可能寫一個安全的可變參數方法的狀況下,就像第147頁的toArray方法那樣。它的列表模擬是List.of方法,因此咱們甚至沒必要編寫它; Java類庫做者已經爲咱們完成了這項工做。 pickTwo方法而後變成這樣:

static <T> List<T> pickTwo(T a, T b, T c) {
    switch(rnd.nextInt(3)) {
      case 0: return List.of(a, b);
      case 1: return List.of(a, c);
      case 2: return List.of(b, c);
    }
    throw new AssertionError();
}

main方變成這樣:

public static void main(String[] args) {
    List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}

生成的代碼是類型安全的,由於它只使用泛型,不是數組。

總而言之,可變參數和泛型不能很好地交互,由於可變參數機制是在數組上面構建的脆弱的抽象,而且數組具備與泛型不一樣的類型規則。 雖然泛型可變參數不是類型安全的,但它們是合法的。 若是選擇使用泛型(或參數化)可變參數編寫方法,請首先確保該方法是類型安全的,而後使用@SafeVarargs註解對其進行標註,以避免形成使用不愉快。

相關文章
相關標籤/搜索