【Effective Java】條25:列表優先於數組

數組和列表有兩個很大的不同:
1. 數組是協變,列表是不變的。意思是當類A是類B的子類時,則A[]B[]的子類;而對於列表,對於任何兩個不同的類型Type1Type2,都不會存在List<Type1>List<Type2>的子類或者父類。

  1. 數組類型是具體的,即數組明確知道元素的類型並強制在運行時確定元素的類型;列表是擦除的,所以在編譯的時候就會明確要求類型統一。如:
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";  //拋出ArrayStoreException

List<Object> ol = new ArrayList<Long>();  //編譯不通過
ol.add("I don't fit in");

以上代碼進行比較,相信大家都會選擇採用列表的方式,畢竟錯誤是越早發現越好。

由於以上兩個原因,所以數組和泛型不能很好的混合一起工作。最明顯的就是,你不能創建泛型的數組,如:new List<E>[]new List<String>[]new E[]都是錯誤的,在IDE中都會提示Error Java:創建泛型數組

那爲什麼不能創建泛型數組呢?最主要原因就是類型安全問題。因爲編譯器在其他正確的程序中發生的轉換就會在運行時失敗,並拋出ClassCastException異常。如下所示:

List<String>[] stringLists = new List<String>[1];   //(1)其實編譯不通過,我們假設可以運行,以此說明問題
List<Integer> intList = Arrays.asList(42);    //(2)
Object[] objects = stringLists;  //(3)
objects[0] = intList;
String s = stringLists[0].get(0);

以上代碼中,代碼(1)表示新建個泛型數組,代碼(2)新建個列表,且有一個類型爲Integer的元素42,由於數組是協變的,所以代碼(3)成立,就是將objects指向stringLists數組,代碼(4)將代碼(2)新建的列表賦值給objects的首元素,代碼(5)表示通過stringLists獲取其中的首元素,並賦值給String類型。大家想想,Integer賦值給String會產生什麼,ClassCastException就出來了。

由於泛型數組不能創建,導致很多時候比較麻煩,譬如泛型不能返回元素類型的數組、當你在使用多參數方法時提示你的警告等。所以當你遇到泛型數組錯誤的時候,最好的解決辦法就是使用列表進行替換。以如下示例說明,假設有個同步列表listCollections.synchronizedList返回的)和一個函數apply,函數的作用是每次取列表兩個數進行對應操作並返回,如果列表的值爲Integer,則將兩個數相加,如果列表的值爲String,則將兩個值連接。現在需要寫個方法reduce來將列表list應用到方法apply上,reduce方法除了list和函數apply之外,還提供個initVal,表示默認值,如果元素爲String,則爲"",如果爲Integer,則爲0。代碼如下所示:

static Object reduce(List list, Function f, Object initVal) {
    synchronized(list) {
        Object result = initVal;
        for (Object o : list) {
            result = f.apply(result, o);
        }

        return result;
    }
}

interface Function {
    Object apply(Object arg1, Object arg2);
}

以上代碼中,由於list是同步列表synchronizedList,所以我們可以直接採用同步列表的內置鎖。從另一方面來看,上面的代碼同步塊中包含了其餘的代碼,其實僅僅在遍歷的時候需要,調用apply的時候都不需要的,導致鎖範圍太大。優化後:

static Object reduce(List list, Function f, Object initVal) {
    Object[] snapShot = list.toArray();

    Object result = initVal;
    for (Object o : snapShot) {
        result = f.apply(result, o);
    }

    return result;
}

考慮到列表元素可能是Integer或者String等類型,且reduce方法儘量不要使用原型類型。如果改爲了泛型:

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    E[] snapShot = (E[])list.toArray();

    E result = initVal;
    for (E o : snapShot) {
        result = f.apply(result, o);
    }

    return result;
}

interface Function<T> {
    T apply(T arg1, T arg2);
}

此時,代碼經過檢驗過後,會給出警告warning: [unchecked] unchecked cast found :Object[], required: E[] E[] snapshot = (E[]) list.toArray();。這個時候由於程序是沒有問題的,只能在E[] snapShot = (E[])list.toArray()上註解SupressWarning

其實如果至始至終只按照列表的操作,可以按如下所示:

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    List<E> snapList;

    synchronized(list) {
        snapList = new ArrayList<E>(list);
    }

    E result = initVal;
    for (E o : snapList) {
        result = f.apply(result, o);
    }

    return result;
}

interface Function<T> {
    T apply(T arg1, T arg2);
}

綜上所述,數組和泛型擁有不同的類型規則。數組是協變並具象的,泛型是不變和擦除性質的。導致的結果就是數組在運行時提供安全性檢查但是編譯時並不能,而列表正相反。通常來說,數組和泛型並不能很好的混合使用,如果你發現你已經混合使用了,且在代碼檢查過程中報出了警告,則最好採用列表代替數組。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章