Effective Java 第三版——32.合理地結合泛型和可變參數
Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這裏第一時間翻譯成中文版。供大家學習分享之用。
32. 合理地結合泛型和可變參數
在Java 5中,可變參數方法(條目 53)和泛型都被添加到平臺中,所以你可能希望它們能夠正常交互; 可悲的是,他們並沒有。 可變參數的目的是允許客戶端將一個可變數量的參數傳遞給一個方法,但這是一個脆弱的抽象( leaky abstraction):當你調用一個可變參數方法時,會創建一個數組來保存可變參數;那個應該是實現細節的數組是可見的。 因此,當可變參數具有泛型或參數化類型時,會導致編譯器警告混淆。
回顧條目 28,非具體化( non-reifiable)的類型是其運行時表示比其編譯時表示具有更少信息的類型,並且幾乎所有泛型和參數化類型都是不可具體化的。 如果某個方法聲明其可變參數為非具體化的類型,則編譯器將在該聲明上生成警告。 如果在推斷類型不可確定的可變參數參數上調用該方法,那麽編譯器也會在調用中生成警告。 警告看起來像這樣:
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
註解標註一個方法。 那麽需要做些什麽來確保這一點呢? 回想一下,調用方法時會創建一個泛型數組,以容納可變參數。 如果方法沒有在數組中存儲任何東西(它會覆蓋參數)並且不允許對數組的引用進行轉義(這會使不受信任的代碼訪問數組),那麽它是安全的。 換句話說,如果可變參數數組僅用於從調用者向方法傳遞可變數量的參數——畢竟這是可變參數的目的——那麽該方法是安全的。
值得註意的是,你可以違反類型安全性,即使不會在可變參數數組中存儲任何內容。 考慮下面的泛型可變參數方法,它返回一個包含參數的數組。 乍一看,它可能看起來像一個方便的小工具:
// 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 []
類型的數組。
現在考慮這個測試pickTw
的main
方法:
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
註解對其進行標註,以免造成使用不愉快。
Effective Java 第三版——32.合理地結合泛型和可變參數