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

來源:互聯網
上載者:User

標籤:unsafe   parameter   子類   runtime   有一個   static   必須   public   gpo   

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 collectionstamps.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 - typesafeprivate final Collection<Stamp> stamps = ... ;

從這個聲明中,編譯器知道stamps集合應該只包含Stamp執行個體,並保證它是true,假設你的整個代碼類庫編譯時間不發出(或者抑制;參見條目27)任何警告。 當使用參數化型別聲明聲明stamps時,錯誤的插入會產生一個編譯時間錯誤訊息,告訴你到底發生了什麼錯誤:

Test.java:9: error: incompatible types: Coin cannot be convertedto 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 amember 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 beconverted 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 flexiblestatic int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

無限制萬用字元Set <?>與原始類型Set之間有什麼區別? 問號真的給你放任何東西嗎? 這不是要點,但萬用字元類型是安全的,原始類型不是。 你可以將任何元素放入具有原始類型的集合中,輕易破壞集合的類型不變性(如第119頁上的unsafeAdd方法所示); 你不能把任何元素(除null之外)放入一個Collection <?>中。 試圖這樣做會產生一個像這樣的編譯時間錯誤訊息:

WildCard.java:13: error: incompatible types: String cannot beconverted 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 operatorif (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

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.