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

自Java 5以來,泛型已經成為該語言的一部分。 在泛型之前,你必須轉換從集合中讀取的每個對象。 如果有人不小心插入了錯誤類型的對象,則在運行時可能會失敗。 使用泛型,你告訴編譯器在每個集合中允許哪些類型的對象。 編譯器會自動插入強制轉換,並在編譯時告訴你是否嘗試插入錯誤類型的對象。 這樣做的結果是既安全又清晰的程序,但這些益處,不限於集合,是有代價的。 本章告訴你如何最大限度地提高益處,並將並發症降至最低。
26. 不要使用原始類型
首先,有幾個術語。一個類或接口,它的聲明有一個或多個類型參數( type parameters ),被稱之為泛型類或泛型接口[JLS,8.1.2,9.1.2]。 例如,List接口具有單個類型參數E,表示其元素類型。 接口的全名是List<E>(讀作“E”的列表),但是人們經常稱它為List。 泛型類和接口統稱為泛型類型(generic types)。
每個泛型定義了一組參數化類型(parameterized types),它們由類或接口名稱組成,后跟一個與泛型類型的形式類型參數[JLS,4.4,4.5]相對應的實際類型參數的尖括號“<>”列表。 例如,List<String>(讀作“字符串列表”)是一個參數化類型,表示其元素類型為String的列表。 (String是與形式類型參數E相對應的實際類型參數)。
最后,每個泛型定義了一個原始類型( raw type),它是沒有任何類型參數的泛型類型的名稱[JLS,4.8]。 例如,對應於List<E>的原始類型是List。 原始類型的行為就像所有的泛型類型信息都從類型聲明中被清除一樣。 它們的存在主要是為了與沒有泛型之前的代碼相兼容。
在泛型被添加到Java之前,這是一個典型的集合聲明。 從Java 9開始,它仍然是合法的,但並不是典型的聲明方式了:
// Raw collection type - don't do this!
// My stamp collection. Contains only Stamp instances.
private final Collection stamps = ... ;
如果你今天使用這個聲明,然后不小心把coin實例放入你的stamp集合中,錯誤的插入編譯和運行沒有錯誤(盡管編譯器發出一個模糊的警告):
// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... )); // Emits "unchecked call" warning
直到您嘗試從stamp集合中檢索coin實例時才會發生錯誤:
// Raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hasNext(); )
Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
stamp.cancel();
正如本書所提到的,在編譯完成之后盡快發現錯誤是值得的,理想情況是在編譯時。 在這種情況下,直到運行時才發現錯誤,在錯誤發生后的很長一段時間,以及可能遠離包含錯誤的代碼的代碼中。 一旦看到ClassCastException,就必須搜索代碼類庫,查找將coin實例放入stamp集合的方法調用。 編譯器不能幫助你,因為它不能理解那個說“僅包含stamp實例”的注釋。
對於泛型,類型聲明包含的信息,而不是注釋:
// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ... ;
從這個聲明中,編譯器知道stamps集合應該只包含Stamp實例,並保證它是true,假設你的整個代碼類庫編譯時不發出(或者抑制;參見條目27)任何警告。 當使用參數化類型聲明聲明stamps時,錯誤的插入會生成一個編譯時錯誤消息,告訴你到底發生了什么錯誤:
Test.java:9: error: incompatible types: Coin cannot be converted
to Stamp
c.add(new Coin());
^
當從集合中檢索元素時,編譯器會為你插入不可見的強制轉換,並保證它們不會失敗(再假設你的所有代碼都不會生成或禁止任何編譯器警告)。 雖然意外地將coin實例插入stamp集合的預期可能看起來很牽強,但這個問題是真實的。 例如,很容易想象將BigInteger放入一個只包含BigDecimal實例的集合中。
如前所述,使用原始類型(沒有類型參數的泛型)是合法的,但是你不應該這樣做。 如果你使用原始類型,則會喪失泛型的所有安全性和表達上的優勢。 鑒於你不應該使用它們,為什么語言設計者首先允許原始類型呢? 答案是為了兼容性。 泛型被添加時,Java即將進入第二個十年,並且有大量的代碼沒有使用泛型。 所有這些代碼都是合法的,並且與使用泛型的新代碼進行交互操作被認為是至關重要的。 將參數化類型的實例傳遞給為原始類型設計的方法必須是合法的,反之亦然。 這個需求,被稱為遷移兼容性,驅使決策支持原始類型,並使用擦除來實現泛型(條目 28)。
雖然不應使用諸如List之類的原始類型,但可以使用參數化類型來允許插入任意對象(如List<Object>)。 原始類型List和參數化類型List<Object>之間有什么區別? 松散地說,前者已經選擇了泛型類型系統,而后者明確地告訴編譯器,它能夠保存任何類型的對象。 雖然可以將List<String>傳遞給List類型的參數,但不能將其傳遞給List<Object>類型的參數。 泛型有子類型的規則,List<String>是原始類型List的子類型,但不是參數化類型List<Object>的子類型(條目 28)。 因此,如果使用諸如List之類的原始類型,則會丟失類型安全性,但是如果使用參數化類型(例如List <Object>)則不會。
為了具體說明,請考慮以下程序:
// Fails at runtime - unsafeAdd method uses a raw type (List)!
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // Has compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
此程序可以編譯,它使用原始類型列表,但會收到警告:
Test.java:10: warning: [unchecked] unchecked call to add(E) as a
member of the raw type List
list.add(o);
^
實際上,如果運行該程序,則當程序嘗試調用strings.get(0)的結果(一個Integer)轉換為一個String時,會得到ClassCastException異常。 這是一個編譯器生成的強制轉換,因此通常會保證成功,但在這種情況下,我們忽略了編譯器警告並付出了代價。
如果用unsafeAdd聲明中的參數化類型List <Object>替換原始類型List,並嘗試重新編譯該程序,則會發現它不再編譯,而是發出錯誤消息:
Test.java:5: error: incompatible types: List<String> cannot be
converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42));
你可能會試圖使用原始類型來處理元素類型未知且無關緊要的集合。 例如,假設你想編寫一個方法,它需要兩個集合並返回它們共同擁有的元素的數量。 如果是泛型新手,那么您可以這樣寫:
// Use of raw type for unknown element type - don't do this!
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
}
這種方法可以工作,但它使用原始類型,這是危險的。 安全替代方式是使用無限制通配符類型(unbounded wildcard types)。 如果要使用泛型類型,但不知道或關心實際類型參數是什么,則可以使用問號來代替。 例如,泛型類型Set<E>的無限制通配符類型是Set <?>(讀取“某種類型的集合”)。 它是最通用的參數化的Set類型,能夠保持任何集合。 下面是numElementsInCommon方法使用無限制通配符類型聲明的情況:
// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
無限制通配符Set <?>與原始類型Set之間有什么區別? 問號真的給你放任何東西嗎? 這不是要點,但通配符類型是安全的,原始類型不是。 你可以將任何元素放入具有原始類型的集合中,輕易破壞集合的類型不變性(如第119頁上的unsafeAdd方法所示); 你不能把任何元素(除null之外)放入一個Collection <?>中。 試圖這樣做會產生一個像這樣的編譯時錯誤消息:
WildCard.java:13: error: incompatible types: String cannot be
converted to CAP#1
c.add("verboten");
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
不可否認的是,這個錯誤信息留下了一些需要的東西,但是編譯器已經完成了它的工作,不管它的元素類型是什么,都不會破壞集合的類型不變性。 你不僅可以將任何元素(除null以外)放入一個Collection <?>中,但是不能保證你所得到的對象的類型。 如果這些限制是不可接受的,可以使用泛型方法(條目 30)或有限制配符類型(條目 31)。
對於不應該使用原始類型的規則,有一些小例外。 你必須在類字面值(class literals)中使用原始類型。 規范中不允許使用參數化類型(盡管它允許數組類型和基本類型)[JLS,15.8.2]。 換句話說,List.class,String [] .class和int.class都是合法的,但List <String> .class和List <?>.class不是合法的。
規則的第二個例外涉及instanceof操作符。 因為泛型類型信息在運行時被刪除,所以在無限制通配符類型以外的參數化類型上使用instanceof運算符是非法的。 使用無限制通配符類型代替原始類型不會以任何方式影響instanceof運算符的行為。 在這種情況下,尖括號和問號就顯得多余。 以下是使用泛型類型的instanceof運算符的首選方法:
// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> s = (Set<?>) o; // Wildcard type
...
}
請注意,一旦確定o對象是一個Set,則必須將其轉換為通配符Set <?>,而不是原始類型Set。 這是一個強制轉換,所以不會導致編譯器警告。
總之,使用原始類型可能導致運行時異常,所以不要使用它們。 它們僅用於與泛型引入之前的傳統代碼的兼容性和互操作性。 作為一個快速回顧,Set<Object>是一個參數化類型,表示一個可以包含任何類型對象的集合,Set<?>是一個通配符類型,表示一個只能包含某些未知類型對象的集合,Set是一個原始類型,它不在泛型類型系統之列。 前兩個類型是安全的,最后一個不是。
為了快速參考,下表中總結了本條目(以及本章稍后介紹的一些)中介紹的術語:
| 術語 | 中文含義 | 舉例 | 所在條目 |
|---|---|---|---|
| Parameterized type | 參數化類型 | List<String> |
條目 26 |
| Actual type parameter | 實際類型參數 | String |
條目 26 |
| Generic type | 泛型類型 | List<E> |
條目 26 |
| Formal type parameter | 形式類型參數 | E |
條目 26 |
| Unbounded wildcard type | 無限制通配符類型 | List<?> |
條目 26 |
| Raw type | 原始類型 | List |
條目 26 |
| Bounded type parameter | 限制類型參數 | <E extends Number> |
條目 29 |
| Recursive type bound | 遞歸類型限制 | <T extends Comparable<T>> |
條目 30 |
| Bounded wildcard type | 限制通配符類型 | List<? extends Number> |
條目 31 |
| Generic method | 泛型方法 | static <E> List<E> asList(E[] a) |
條目 30 |
| Type token | 類型令牌 | String.class |
條目 33 |
