Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
數組在兩個重要方面與泛型不同。 首先,數組是協變的(covariant)。 這個嚇人的單詞意味着如果Sub
是Super
的子類型,則數組類型Sub []
是數組類型Super []
的子類型。 相比之下,泛型是不變的(invariant):對於任何兩種不同的類型Type1
和Type2
,List<Type1>
既不是List <Type2>
的子類型也不是父類型。[JLS,4.10; Naftalin07,2.5]。 你可能認為這意味着泛型是不足的,但可以說是數組缺陷。 這段代碼是合法的:
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
但這個不是:
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
無論哪種方式,你不能把一個String類型放到一個Long類型容器中,但是用一個數組,你會發現在運行時產生了一個錯誤;對於列表,可以在編譯時就能發現錯誤。 當然,你寧願在編譯時找出錯誤。
數組和泛型之間的第二個主要區別是數組被具體化了(reified)[JLS,4.7]。 這意味着數組在運行時知道並強制執行它們的元素類型。 如前所述,如果嘗試將一個String放入Long數組中,得到一個ArrayStoreException異常。 相反,泛型通過擦除(erasure)來實現[JLS,4.6]。 這意味着它們只在編譯時執行類型約束,並在運行時丟棄(或擦除)它們的元素類型信息。 擦除是允許泛型類型與不使用泛型的遺留代碼自由互操作(條目 26),從而確保在Java 5中平滑過渡到泛型。
由於這些基本差異,數組和泛型不能很好地在一起混合使用。 例如,創建泛型類型的數組,參數化類型的數組,以及類型參數的數組都是非法的。 因此,這些數組創建表達式都不合法:new List <E> []
,new List <String> []
,new E []
。 所有將在編譯時導致泛型數組創建錯誤。
為什么創建一個泛型數組是非法的? 因為它不是類型安全的。 如果這是合法的,編譯器生成的強制轉換程序在運行時可能會因為ClassCastException異常而失敗。 這將違反泛型類型系統提供的基本保證。
為了具體說明,請考慮下面的代碼片段:
// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
讓我們假設第1行創建一個泛型數組是合法的。第2行創建並初始化包含單個元素的List<Integer>
。第3行將List<String>
數組存儲到Object數組變量中,這是合法的,因為數組是協變的。第4行將List <Integer>
存儲在Object數組的唯一元素中,這是因為泛型是通過擦除來實現的:List<Integer>
實例的運行時類型僅僅是List,而List<String> []
實例是List []
,所以這個賦值不會產生ArrayStoreException異常。現在我們遇到了麻煩。將一個List<Integer>
實例存儲到一個聲明為僅保存List<String>
實例的數組中。在第5行中,我們從這個數組的唯一列表中檢索唯一的元素。編譯器自動將檢索到的元素轉換為String,但它是一個Integer,所以我們在運行時得到一個ClassCastException異常。為了防止發生這種情況,第1行(創建一個泛型數組)必須產生一個編譯時錯誤。
類型E
,List<E>
和List<String>
等在技術上被稱為不可具體化的類型(nonreifiable types)[JLS,4.7]。 直觀地說,不可具體化的類型是其運行時表示包含的信息少於其編譯時表示的類型。 由於擦除,可唯一確定的參數化類型是無限定通配符類型,如List <?>
和Map <?, ?>
(條目 26)。 盡管很少有用,創建無限定通配符類型的數組是合法的。
禁止泛型數組的創建可能會很惱人的。 這意味着,例如,泛型集合通常不可能返回其元素類型的數組(但是參見條目 33中的部分解決方案)。 這也意味着,當使用可變參數方法(條目 53)和泛型時,會產生令人困惑的警告。 這是因為每次調用可變參數方法時,都會創建一個數組來保存可變參數。 如果此數組的元素類型不可確定,則會收到警告。 SafeVarargs
注解可以用來解決這個問題(條目 32)。
當你在強制轉換為數組類型時,得到泛型數組創建錯誤,或是未經檢查的強制轉換警告時,最佳解決方案通常是使用集合類型List <E>
而不是數組類型E []
。 這樣可能會犧牲一些簡潔性或性能,但作為交換,你會獲得更好的類型安全性和互操作性。
例如,假設你想用帶有集合的構造方法來編寫一個Chooser
類,並且有個方法返回隨機選擇的集合的一個元素。 根據傳遞給構造方法的集合,可以使用選擇器作為游戲模具,魔術8球或數據源進行蒙特卡羅模擬。 這是一個沒有泛型的簡單實現:
// Chooser - a class badly in need of generics!
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
要使用這個類,每次調用方法時,都必須將Object的choose
方法的返回值轉換為所需的類型,如果類型錯誤,則轉換在運行時失敗。 我們先根據條目 29的建議,試圖修改Chooser類,使其成為泛型的。
// A first cut at making Chooser generic - won't compile
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choices.toArray();
}
// choose method unchanged
}
如果你嘗試編譯這個類,會得到這個錯誤信息:
Chooser.java:9: error: incompatible types: Object[] cannot be
converted to T[]
choiceArray = choices.toArray();
^
where T is a type-variable:
T extends Object declared in class Chooser
沒什么大不了的,將Object數組轉換為T數組:
choiceArray = (T[]) choices.toArray();
這沒有了錯誤,而是得到一個警告:
Chooser.java:9: warning: [unchecked] unchecked cast
choiceArray = (T[]) choices.toArray();
^
required: T[], found: Object[]
where T is a type-variable:
T extends Object declared in class Chooser
編譯器告訴你在運行時不能保證強制轉換的安全性,因為程序不會知道T代表什么類型——記住,元素類型信息在運行時會被泛型刪除。 該程序可以正常工作嗎? 是的,但編譯器不能證明這一點。 你可以證明這一點,在注釋中提出證據,並用注解來抑制警告,但最好是消除警告的原因(條目 27)。
要消除未經檢查的強制轉換警告,請使用列表而不是數組。 下面是另一個版本的Chooser類,編譯時沒有錯誤或警告:
// List-based Chooser - typesafe
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
這個版本有些冗長,也許運行比較慢,但是值得一提的是,在運行時不會得到ClassCastException
異常。
總之,數組和泛型具有非常不同的類型規則。 數組是協變和具體化的; 泛型是不變的,類型擦除的。 因此,數組提供運行時類型的安全性,但不提供編譯時類型的安全性,反之亦然。 一般來說,數組和泛型不能很好地混合工作。 如果你發現把它們混合在一起,得到編譯時錯誤或者警告,你的第一個沖動應該是用列表來替換數組。