1. 程式人生 > >Effective Java 3rd 條目26 不要使用原生型別

Effective Java 3rd 條目26 不要使用原生型別

首先,一些術語。類或者介面,它的宣告有一個或者多個型別引數(type parameter),是泛型(generic)類或者介面[JLS, 8.1.2, 9.1.2]。例如,List介面有一個型別引數,E,代表它的元素型別。這個介面的完整名字是List<E>(讀作“E的列表”),但是人們常常簡單地叫它為List。泛型類和介面全部叫做泛型型別。

每個泛型型別定義了引數化(parameterized)型別的集,它包含了類或者介面名字,緊跟著實際型別引數(actual type parameter)的尖括號列表,這些型別引數是相對於泛型型別的形式型別引數[JLS, 4.4, 4.5]。例如,List<String>(讀作“字串的列表”)是一個引數化型別,代表元素是String型別的一個列表。(String 是相對於形式型別引數E的實際型別引數。)

最後,每個泛型引數定義了一個原生型別(raw type),它是泛型型別的名字,而沒有任何相應的引數型別[JLS, 4.8]。例如,相對於List<E>的原生型別是List。原生型別的行為就像所有的泛型型別資訊從型別宣告中擦除。它們主要是為了和以前的泛型程式碼相相容而存在。

在泛型加入到Java之前,以下是一個典型的集合宣告。在Java9中,它仍舊是合法的,但是遠非可仿效的:

// 原生型別 - 不要這麼做!

// 我的郵票集合。僅僅包含Stamp例項。 
private final Collection stamps = ... ;

如果你今天使用這個宣告,而且意外地把硬幣放入到你的郵票集合中,那麼這個錯誤的插入正常編譯,而且執行沒有錯誤(儘管編譯器發出一個模糊警告):

// 硬幣錯誤插入到郵票集合 
stamps.add(new Coin( ... )); // 發出“非受檢查的呼叫”的警告

你不會遇見一個錯誤,直到你試著從郵票集合中獲取這個硬幣:

// 原生迭代器型別 - 不要這麼用!
for (Iterator i = stamps.iterator(); i.hasNext(); )
    Stamp stamp = (Stamp) i.next(); // 丟擲ClassCastException 
        stamp.cancel();

就像這本書自始至終提到的,在構建它們之後,最好在編譯的時候,儘早發現錯誤是值得的。這這個情況中,你直到執行時才會發現錯誤,遠在它發生之後,而且是在遠離於包含這個錯誤的程式碼的程式碼中。一旦你看見了ClassCastException,你不得不搜尋程式碼庫,尋找把這個硬幣放入到郵票集合的方法呼叫。編譯器不會幫你,因為它不能理解註釋說“僅僅包含Stamp例項”。

使用泛型,型別宣告包含這個資訊,而不是註釋:

// 引數化的結合型別 - 型別安全的 
private final Collection<Stamp> stamps = ... ; 

從這個宣告中,編譯器知道stamps應該僅僅包含Stamp例項,而且保證它是正確的,而且你的整個程式碼庫正常編譯,沒有發出(或者抑制;參考條目27)任何警告。當stamps以引數化型別宣告方式宣告時,錯誤插入產生一個編譯時錯誤資訊,精確地告訴你是什麼錯誤:

Test.java:9: error: incompatible types: Coin cannot be converted to Stamp
c.add(new Coin()); 
          ^ 

當你從集合中獲取元素時,編譯器為你插入了不可見的強轉,而且保證了它們不會失敗(再次,你的所有程式碼不會產生或者抑制任何編譯警告)。雖然不慎把硬幣插入到一個郵票集合的情況可能顯得牽強附會的,但是這個問題是實際的。例如,容易想象,把BigInteger放到一個集合,這個集合被認為是隻包含BigDecimal例項。

就像前面提到的,使用原生型別(沒有型別引數的泛型型別)是合法的,但是你不應該這麼做。如果你使用原生型別,那麼是失去了泛型的所有優點:安全型別和表達性。既然你不應該使用它們,那麼為什麼語言設計者起初允許原生型別呢?為了相容性。當泛型加入時,Java快進入了它的第二個十年,有未使用泛型的大量程式碼已經存在。這些程式碼仍舊是合法的,而且和使用泛型的更新程式碼可以互操作,這注定是至關重要的。它不得不是合法的:把引數化型別的例項傳遞到一個方法,這個方法是設計為原生型別使用的,或者相反。這個要求,被稱為遷移相容性(migration compatibility),使得做出了這個決定:支援原生型別和使用擦除(erasure)實現泛型(條目28)。

雖然你不應該使用原生型別,比如List,但是使用引數化為允許插入任意物件的型別,比如List<Object>,這是合適的。那麼原生型別List和引數化型別List<Object>的區別是什麼?大致地講,前者不在泛型型別系統中,而後者則顯式地告訴編譯器,它可以留存任何型別的物件。雖然你可以把List<String>傳遞到型別引數List,但是你不能把它傳遞到型別引數列表List<Object>。泛型有子型別化的規則,List<String>是原始型別List的子型別,但不是引數化型別List<Object>的子型別(條目28)。因此,如果你使用了原生型別,比如List,那麼你失去了型別安全,但是如果你使用引數化型別,比如List<Object&gt,那麼你沒有失去

為了使得這個具體,考慮下面的程式:

// 執行時失敗 - unsafeAdd方法使用了原生型別(List)!
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // 編譯器產生的強轉 
}

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>代替原生型別,而且嘗試重新編譯這個程式,那麼你將發現它不再能編譯而且丟擲錯誤資訊:

Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42)); 
    ^

你可能想著為一個集合使用原生型別,這個集合的元素型別是未知的而且也不重要。例如,假設你想編寫一個接受了兩個集的方法,而且返回了它們相同元素的個數。如果你對泛型不熟悉,以下是你可能這麼編寫這樣的方法:

// 使用未知元素型別的原生型別 - 不要這麼做!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1)) 
            result++;
    return result; 
}

這個方法起作用,但是它使用了原生型別,這是危險的。安全的替代方案是,使用非受限萬用字元(unbounded wildcard)型別。如果你想使用一個泛型型別,但是你不知道也不關心實際型別引數是什麼,那麼你可以用問號替代。例如,泛型型別Set<E>的非受限萬用字元型別是 Set<?>(讀做“某個型別的集”)。這個是最通用的引數化Set型別,它可以保留任何集。以下是使用非受限萬用字元型別的numElementsInCommon宣告看上去的樣子:

// 使用無上限萬用字元型別 - 型別安全的和靈活的
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<?>中,而且你不能假設關於你獲得物件的型別的任何事情。如果這些限制是不能接受的,那麼你應該使用泛型方法(generic method)(條目30),或者受限通配(bounded wildcard)型別(條目31)。

對於你不應該使用原生型別這個規則,有一些小小的例外。你必須在類字面常量中使用原生型別。這個規範不允許使用引數化型別(儘管他不允許array型別和原始型別)[JLS, 15.8.2]。換句話說,List.class、String[].class和int.class全是合法的,但是List<String>.class和List<?>.class不是合法的。

第二個例外是,這個規則是關於instanceof操作子。因為泛型型別資訊在執行時擦除,所以在引數化型別上使用instanceof操作子是不合法的,而不是非受限萬用字元型別。非受限萬用字元型別,代替原生型別的使用時,在任何情況下都不會影響instanceof操作子的行為。這種情況下,尖括號和問號只是噪音。以下是使用用泛型型別的instanceof操作子的更可取的方法

// 原生型別的合法使用 - instanceof操作子
if (o instanceof Set) { // 原生型別
    Set<?> s = (Set<?>) o; // 萬用字元型別
    ... 
} 

注意到,一旦你決定o是一個Set,那麼你必須把它強轉到萬用字元型別Set<?>,而不是原生型別Set。這是一個受檢查的強轉,所以它不會造成一個編譯器警告。

總之,使用原生型別可能導致執行時異常,所以不要使用它們。它們僅僅是為了相容性和遺留程式碼的互操作而存在,這個遺留程式碼早於泛型的引入。快速回顧下,Set<Object>是引數化型別,代表一個可以包含任何型別的物件;Set<?>是萬用字元型別,代表僅僅包含某個未知型別的物件;Set是一個原生型別,它從泛型型別系統退出了。前兩個是安全的,最後一個則不是。

作為一個快速參考,這個條目中(一些由這章後續引入)引入的術語總結在如下表格中:

術語 例子 條目
引數化型別 List<String> 條目 26
實際型別引數 String 條目 26
泛型型別 List<E> 條目 26, 29
形式型別引數 E 條目 26
非受限萬用字元型別 List<?> 條目 26
原生型別 List 條目 26
受限型別引數 <E extends Number> 條目 29
迴圈型別受限 <T extends Comparable<T>> 條目 30
受限萬用字元型別 List<? extends Number> 條目 31
泛型方法 static <E> List<E> asList(E[] a) Item 30
型別標記 String.class 條目 33