Effective Java 第三版——26. 不要使用原始類型

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

Effective Java, Third Edition

自Java 5以來,泛型已經成爲該語言的一部分。 在泛型以前,你必須轉換從集合中讀取的每一個對象。 若是有人不當心插入了錯誤類型的對象,則在運行時可能會失敗。 使用泛型,你告訴編譯器在每一個集合中容許哪些類型的對象。 編譯器會自動插入強制轉換,並在編譯時告訴你是否嘗試插入錯誤類型的對象。 這樣作的結果是既安全又清晰的程序,但這些益處,不限於集合,是有代價的。 本章告訴你如何最大限度地提升益處,並將併發症降至最低。git

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集合中,錯誤的插入編譯和運行沒有錯誤(儘管編譯器發出一個模糊的警告):flex

// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... )); // Emits "unchecked call" warning

直到您嘗試從stamp集合中檢索coin實例時纔會發生錯誤:this

// 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.classString [] .classint.class都是合法的,但List <String> .classList <?>.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
相關文章
相關標籤/搜索