Effective Java 第三版——28. 列表優於數組

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

Effective Java, Third Edition

數組在兩個重要方面與泛型不一樣。 首先,數組是協變的(covariant)。 這個嚇人的單詞意味着若是SubSuper的子類型,則數組類型Sub []是數組類型Super []的子類型。 相比之下,泛型是不變的(invariant):對於任何兩種不一樣的類型Type1Type2List<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類型容器中,可是用一個數組,你會發如今運行時產生了一個錯誤;對於列表,能夠在編譯時就能發現錯誤。 固然,你寧願在編譯時找出錯誤。dom

數組和泛型之間的第二個主要區別是數組被具體化了(reified)[JLS,4.7]。 這意味着數組在運行時知道並強制執行它們的元素類型。 如前所述,若是嘗試將一個String放入Long數組中,獲得一個ArrayStoreException異常。 相反,泛型經過擦除(erasure)來實現[JLS,4.6]。 這意味着它們只在編譯時執行類型約束,並在運行時丟棄(或擦除)它們的元素類型信息。 擦除是容許泛型類型與不使用泛型的遺留代碼自由互操做(條目 26),從而確保在Java 5中平滑過渡到泛型。性能

因爲這些基本差別,數組和泛型不能很好地在一塊兒混合使用。 例如,建立泛型類型的數組,參數化類型的數組,以及類型參數的數組都是非法的。 所以,這些數組建立表達式都不合法:new List <E> []new List <String> []new E []。 全部將在編譯時致使泛型數組建立錯誤。學習

爲何建立一個泛型數組是非法的? 由於它不是類型安全的。 若是這是合法的,編譯器生成的強制轉換程序在運行時可能會由於ClassCastException異常而失敗。 這將違反泛型類型系統提供的基本保證。ui

爲了具體說明,請考慮下面的代碼片斷:翻譯

// 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行(建立一個泛型數組)必須產生一個編譯時錯誤。code

類型EList<E>List<String>等在技術上被稱爲不可具體化的類型(nonreifiable types)[JLS,4.7]。 直觀地說,不可具體化的類型是其運行時表示包含的信息少於其編譯時表示的類型。 因爲擦除,可惟一肯定的參數化類型是無限定通配符類型,如List <?>Map <?, ?>(條目 26)。 儘管不多有用,建立無限定通配符類型的數組是合法的。blog

禁止泛型數組的建立可能會很惱人的。 這意味着,例如,泛型集合一般不可能返回其元素類型的數組(可是參見條目 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異常。

總之,數組和泛型具備很是不一樣的類型規則。 數組是協變和具體化的; 泛型是不變的,類型擦除的。 所以,數組提供運行時類型的安全性,但不提供編譯時類型的安全性,反之亦然。 通常來講,數組和泛型不能很好地混合工做。 若是你發現把它們混合在一塊兒,獲得編譯時錯誤或者警告,你的第一個衝動應該是用列表來替換數組。

相關文章
相關標籤/搜索