Effective Java——泛型

                        目錄
二十三、請不要在新代碼中使用原生態類型
二十四、消除非受檢警告
二十五、列表優先於數組
二十六、優先考慮泛型
二十七、優先考慮泛型方法
二十八、利用有限制通配符來提升API的靈活性
二十九、優先考慮類型安全的異構容器


二十三、請不要在新代碼中使用原生態類型

        聲明中具有一個或者多個類型參數的類或者接口,就是泛型類或接口,如List<E>,這其中E表示List集合中元素的類型。在Java中,相對於每個泛型類都有一個原生類與之對應,即不帶任何實際類型參數的泛型名稱,如List<E>的原生類型List。他們之間最爲明顯的區別在於List<E>包含的元素必須是E(泛型)類型,如List<String>,那麼他的元素一定是String,否則將產生編譯錯誤。和泛型不同的是,原生類型List可以包含任何類型的元素,因此在向集合插入元素時,即使插入了不同類型的元素也不會引起編譯期錯誤。那麼在運行,當List的使用從List中取出元素時,將不得不針對類型作出判斷,以保證在進行元素類型轉換時不會拋出ClassCastException異常。由此可以看出,泛型集合List<E>不僅可以在編譯期發現該類錯誤,而且在取出元素時不需要再進行類型判斷,從而提高了程序的運行時效率。

//原生類型的使用方式
class TestRawType {
    private final List stamps = new List();
    public static void main(String[] args) {
        stamps.add(new Coin(...));
        for (Iterator i = stamps.iterator(); i.hasNext(); ) {
            Stamp s = (Stamp)i.next();  //這裏將拋出類型轉換異常
            //TODO: do something.
        }
    }
}

//泛型類型的使用方式
class TestGenericType {
    private final List<Stamp> stamps = new List<Stamp>();
    public static void main(String[] args) {
        stamps.add(new Coin(...)); //該行將直接導致編譯錯誤。
        for (Stamp s : stamps) { //這裏不再需要類型轉換了。
            //TODO: do something
        }
    }
}

        可以看出,泛型相對於原生態類型在安全性和表達性方面的有着非常明顯的優勢。使用原生態類型的目的是爲了提供兼容性,畢竟泛型是在Java1.5中才推出的。

        現在我們比較一下List和List<Object>這兩個類型之間的主要區別,儘管這兩個集合可以包含任何類型的對象元素,但是前者是類型不安全的,而後者則明確告訴使用者可以存放任意類型的對象元素。另一個區別是,如果void func(List l)改爲void func(List<Object> l),List<String>類型的對象將不能傳遞給func函數,因爲Java將這兩個泛型類型視爲完全不同的兩個類型。

        在不確定或者不在乎集合中的元素類型的情況下,你也許會使用原生態類型。假設想要編寫一個方法,它有兩個集合(set),並從中返回它們共有的元素的數量,下面是用原生態類型編寫的方法:

static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}

        方法可以使用,但是非常危險,不安全。從Java1.5開始,提供了一種安全的替代方法,稱作無限制的通配符類型,如果要使用泛型,但不確實或者不關心實際的類型參數,就可以使用一個問號代替。如,泛型Set<E>的無限制通配符類型爲Set<?>,這是最普通的參數化Set類型,可以持有任何集合。

static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}

        在新代碼中不要使用原生類型,這條規則有兩個例外,兩者都源於“泛型信息可以在運行時被擦除”這一事實。在Class對象中必須要使用原生類型。JLS不允許使用Class的參數化類型。換句話說,List.class, String[].class和int.class都是合法的,但是List<String>.class和List<?>.class則是不合法。這條規則的第二個例外與instanceof操作符相關。由於泛型信息可以在運行時被擦除,因此在泛型類型上使用instanceof操作符是非法的。下面是利用泛型來使用instanceof操作符的首選方法:

if (o instanceof Set) {
    Set<?> m = (Set<?>)o;
}

        總之,使用原生態類型會在運行時導致異常,因此不要在代碼中使用。Set<Object>是個參數化類型,表示可以包含任何對象類型的一個集合;Set<?>則是一個通配符類型,表示只能包含某種未知對象類型的一個集合;Set則是個原生態類型,它脫離了泛型系統。前兩種是安全的,最後一種不安全。


二十四、消除非受檢警告

        在進行泛型編程時,經常會遇到編譯器報出的非受檢警告(unchecked cast warnings),如:Set<Lark> exaltation = new HashSet(); 對於這樣的警告要儘可能在編譯期予以消除:Set<Lark> exaltation = new HashSet<Lark>();。對於一些比較難以消除的非受檢警告,可以通過@SuppressWarnings("unchecked")註解來禁止該警告,前提是你已經對該條語句進行了認真地分析,確認運行期的類型轉換不會拋出ClassCastException異常。同時要在儘可能小的範圍了應用該註解(SuppressWarnings),如果可以應用於變量,就不要應用於函數。儘可能不要將該註解應用於Class,這樣極其容易掩蓋一些可能引發異常的轉換。見如下代碼:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        return (T[])Arrays.copyOf(elements,size,a.getClass());
    System.arraycopy(elements,0,a,0,size);
    if (a.length > size)
        a[size] = null;
    return a;
}

        編譯該代碼片段時,編譯器會針對(T[])Arrays.copyOf(elements,size,a.getClass())語句產生一條非受檢警告,現在我們需要做的就是添加一個新的變量,並在定義該變量時加入@SuppressWarnings註解,見如下修訂代碼:

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        //TODO: 加入更多的註釋,以便後面的維護者可以非常清楚該轉換是安全的
        @SuppressWarnings("unchecked") T[] result =
            (T[])Arrays.copyOf(elements,size,a.getClass());
        return result;
    }
    System.arraycopy(elements,0,a,0,size);
    if (a.length > size)
        a[size] = null;
    return a;
}
        什麼要消除非受檢警告,還有一個比較重要的原因。在開始的時候,如果工程中存在大量的未消除非受檢警告,開發者認真分析了每一處警告並確認不會產生任何運行時錯誤,然而所差的是在分析之後沒有消除這些警告。那麼在之後的開發中,一旦有新的警告發生,極有可能淹沒在原有的警告中,而沒有被開發者及時發現,最終成爲問題的隱患。如果恰恰相反,在分析之後消除了所有的警告,那麼當有新警告出現時將會立即引起開發者的注意。


二十五、列表優先於數組

        數組和泛型相比,有兩個重要的不同點。首先就是數組是協變的,如:Object[] objArray = new Long[10]是合法的,因爲Long是Object的子類,與之相反,泛型是不可協變的,如List<Object> objList = new List<Long>()是非法的,將無法通過編譯。因此泛型可以保證更爲嚴格的類型安全性,一旦出現插入元素和容器聲明時不匹配的現象是,將會在編譯期報錯。二者的另一個區別是數組是具體化的,因此數組會在運行時才知道並檢查它們的元素類型約束。如將一個String對象存儲在Long的數組中時,就會得到一個ArrayStoreException異常。相比之下,泛型則是通過擦除來實現的。因此泛型只是在編譯時強化類型信息,並在運行時丟棄它們的元素類型信息。擦除就是使泛型可以與沒有使用泛型的代碼隨意進行交互。由此可以得出混合使用泛型和數組是比較危險的,因爲Java的編譯器禁止了這樣的使用方法,一旦使用,將會報編譯錯誤。

        當你得到泛型數組創建錯誤時,最好的解決辦法通常是優先使用集合類型List<E>,而不是數組類型E[]。這樣可能會損失一些性能或簡潔性,但是換回的卻是更高的類型安全性和互用性。

static Object reduce(List l, Function f, Object initVal) {
    Object[] snapshot = l.toArray();
    Object result = initVal;
    for (Object o : snapshot) {
        result = f.apply(result,o);
    }
    return result;
}

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

        從以上函數和接口的定義可以看出,如果他們被定義成泛型函數和泛型接口,將會得到更好的類型安全,同時也沒有對他們的功能造成任何影響,見如下修改爲泛型的示例代碼:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {
    E[] snapshot = l.toArray();
    E result = initVal;
    for (E e : snapshot) {
        result = f.apply(result,e);
    }
    return result;
}

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

        這樣的寫法回提示一個編譯錯誤,即E[] snapshot = l.toArray();是無法直接轉換並賦值的。修改方式也很簡單,直接強轉就可以了,如E[] snapshot = (E[])l.toArray();在強轉之後,仍然會收到編譯器給出的一條警告信息,即無法在運行時檢查轉換的安全性。儘管結果證明這樣的修改之後是可以正常運行的,但是這樣的寫法確實也是不安全的,更好的辦法是通過List<E>替換E[],見如下修改後的代碼:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {
    List<E> snapshot;
    synchronized(l) {
        snapshot = new ArrayList<E>(l);
    }
    E result = initVal;
    for (E e : snapshot) {
        result = f.apply(result,e);
    }
    return result;
}

        總而言之,數組和泛型有着不同的類型規則。數組是協變且可以具體變化的;泛型是不可變的且可以被擦除的。因此,數組提供了運行時的類型安全,但是沒有編譯時的類型安全,泛型則反之。一般來說,數組和泛型不能很好地混合使用。如果你發現自己將它們混合起來使用,並且得到了編譯時錯誤或者警告,你的第一反應應該是用列表代替數組。


二十六、優先考慮泛型

        如第6條中的這個簡單的堆棧實現:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
    public boolean isEmpty() {
        return size == 0;
    }
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements,2 * size + 1);
    }
}

        再看與之相對於的泛型集合實現方式:

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    } 
    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null;
        return result;
    }
    public boolean isEmpty() {
        return size == 0;
    }
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements,2 * size + 1);
    }
}

        上面的泛型集合類Stack<E>在編譯時會引發一個編譯錯誤,即elements = new  E[DEFAULT_INITIAL_CAPACITY]語句不能直接創建不可具體化類型的數組。第一種修改方式如下:創建Object數組,並將其轉換成泛型數組類型,只要我們保證所有push到該數組中的對象均爲該類型的對象即可,剩下需要做的就是添加註解以消除該警告:

@SuppressWarning("unchecked")
public Stack() {
    elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}

        第二種修改方法是:將elements域的類型從E[]改爲Object[],並將從數組中獲取的元素由Object轉換成E,最後在包含未受檢轉換的任務上禁止警告,而不是在整個方法上:

public class Stack<E> {
    private Object[] elements;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        @SuppressWarning("unchecked") E result = (E)elements[--size];
        elements[size] = null;
        return result;
    }
}

        由於禁止數組類型的未受檢比禁止標量類型的更加危險,所以第二種案發更加安全。但是在實際的泛型類中,代碼中會有多個地方需要從數組中讀出元素,而選擇第二種方案需要多次轉換成E,這是第一種方案更加常用的原因。

        第25條的優先使用列表與此矛盾,實際上並不能總想在泛型中使用列表。有些泛型如ArrayList,則必須在數組上實現。爲了提升性能,其他泛型如HashMap也在數組上實現。注意:泛型的類型參數不能爲基本類型:企圖創建Stack<int>或者Stack<double>會產生編譯時錯誤,這是Java泛型系統根本的侷限性,可以通過使用基本包裝類型來避開這條限制。

        總而言之,使用泛型比使用需要在客戶端代碼中進行轉換的類型來的更加安全,也更加容易。在設計新類型的時候,要確保它們不需要這種轉換就可以使用。這通常意味着要把類做成是泛型的。


二十七、優先考慮泛型方法

        和優先選用泛型類一樣,我們也應該優先選用泛型方法。特別是靜態工具方法尤其適合於泛型化。如Collections.sort()和Collections.binarySearch()等靜態方法都泛型化了。下面這個方法返回兩個集合的聯合:

public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

        這個方法在編譯時會有警告報出。爲了修正這些警告,最好的方法就是使該方法變爲類型安全的,要將方法聲明修改爲聲明一個類型參數,表示這三個集合的元素類型,並在方法中使用類型參數,見如下修改後的泛型方法代碼:

public static <E> Set<E> union(Set<E> s1,Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

        對於簡單的泛型方法,這樣修改後不會產生任何警告,並提供了類型安全性,也更容易使用:

public static void main(String[] args) {
    Set<String> guys = new HashSet<String>(Arrays.asList("Tom", "Dick", "Harry"));
    Set<String> stooges = new HashSet<String>(Arrays.asList("Larry", "Moe", "Curly"));
    Set<String> aflCio = union(guys, stooges);
    System.out.println(aflCio);
}
        和調用泛型對象構造函數來創建泛型對象不同的是,在調用泛型函數時無須指定函數的參數類型,而是通過Java編譯器的類型推演來填充該類型信息,見如下泛型對象的構造:Map<String,List<String>> anagrams = new HashMap<String,List<String>>();

        很明顯,以上代碼在等號的兩邊都顯示的給出了類型參數,並且必須是一致的。爲了消除這種重複,可以編寫一個泛型靜態工廠方法,與想要使用的每個構造器相對應,如:

public static <K,V> HashMap<K,V> newHashMap() {
    return new HashMap<K,V>();
}

        我們的調用方式也可以改爲:Map<String,List<String>> anagrams = newHashMap();

        除了在以上的情形下使用泛型函數之外,我們還可以在泛型單例工廠的模式中應用泛型函數,這些函數通常爲無狀態的,且不直接操作泛型對象的方法,見如下示例:

public interface UnaryFunction<T> {
    T apply(T arg);
}

class SingletonFactory {
    private static UnaryFunction<Object> IDENTITY_FUNCTION =
        new UnaryFunction<Object>() {
            public Object apply(Object arg) { return arg; }
        };
    @SuppressWarning("unchecked")
    public static <T> UnaryFunction<T> identityFunction() {
        return (UnaryFunction<T>)IDENTITY_FUNCTION;
    }
}

        調用方式如下:

public static void main(String[] args) {
    String[] strings = {"jute","hemp","nylon"};
    UnaryFunction<String> sameString = identityFunction();
    for (String s : strings)
        System.out.println(sameString.apply(s));

    Number[] numbers = {1,2.0,3L};
    UnaryFunction<Number> sameNumber = identityFunction();
    for (Number n : numbers)
        System.out.println(sameNumber.apply(n)); 
}

        對於該靜態函數,如果我們爲類型參數添加更多的限制條件,如參數類型必須是Comparable<T>的實現類,這樣我們的函數對象便可以基於該接口做更多的操作,而不僅僅是像上例中只是簡單的返回參數對象,見如下代碼:

public static <T extends Comparable<T>> T max(List<T> l) {
    Iterator<T> i = l.iterator();
    T result = i.next();
    while (i.hasNext()) {
        T t = i.next();
        if (t.compareTo(result) > 0)
            result = T;
    }
    return result;
}

        總而言之,泛型方法就想泛型對象一樣,提供了更爲安全的使用方式。


二十八、利用有限制通配符來提升API的靈活性

        前面說過,參數哈類型是不可變的,而有時候,我們需要的靈活性要比不可變類型所能提供的更多,如第26條的堆棧:

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

        現在我們需要增加一個方法:

public void pushAll(Iterable<E> src) {
    for (E e : src)
        push(e);
}

        但如果我們的E類型爲Number,而需要將Integer對象也插入到該容器中,現在的寫法將會導致編譯錯誤,因爲即使Integer是Number的子類,由於類型參數是不可變的,因此這樣的寫法也是錯誤的。需要進行如下的修改:

public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
        此時輸入參數類型爲“E的某個子類型的Iterable接口(每個類型都是自身子類型)”。既然有了pushAll方法,我們可能也需要新增一個popAll的方法與之對應,見如下代碼:
public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

        popAll方法將當前容器中的元素全部彈出,並以此添加到參數集合中。如果Collections中的類型參數和Stack完全一致,這樣的寫法不會有任何問題,然而在實際的應用中,我們通常會將Collection中的元素視爲更通用的對象類型,如Object,見如下應用代碼:

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objs = ...;
numberStack.popAll(objs);

        由於Collection<Object>不是Collection<Number>的子類型,因此會編譯報錯。此時將popAll參數類型改爲Collection<? super E>:E的某種超類的集合。就可以解決問題:

public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

        下面的助記符便於記住要使用哪種通配符類型:PECS(producer-extends, consumer-super)。解釋一下,如果參數化類型表示一個T生產者,就使用<? extends T>,如果它表示一個T消費者,就使用<? super T>。在我們上面的例子中,pushAll的src參數產生E實例供Stack使用,因此src相應的類型爲Iterable<? extends E>;popAll的dst參數通過Stack消費E實例,因此dst相應的類型爲Collection<? super E>。

        現在看看第27條中union方法,下面是聲明:public static <E> Set<E> union(Set<E> s1, Set<E> s2);

        這裏的s1和s2都是生產者,根據PECS原則,它們的聲明可以改爲:public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);

        注意,不要用通配符類型作爲返回類型。修改後的union方法無法進行類型推導,應該增加顯示類型參數:

Set<Integer> integers = new Set<Integer>();
Set<Double> doubles = new Set<Double>();
Set<Number> numbers = Union.<Number>union(integers,doubles);

        現在我們再來看一下前面也給出過的max方法,其初始聲明爲:public static <T extends Comparable<T>> T max<List<T> srcList);

        下面是修改過的使用通配符類的聲明:public static <T extends Comparable<? super T>> T max(List<? extends T> srcList);

        下面將逐一給出新聲明的解釋:
        1.函數參數srcList產生了T實例,因此將類型從List<T>改爲List<? extends T>。
        2.最初T被指定爲擴展Comparable<T>,然而Comparable又是T的消費者,用於比較兩個T之間的順序關係。因此參數化類型Comparable<T>被替換爲Comparable<? super T>。注:Comparator和Comparable一樣,他們始終都是消費者,因此Comparable<? super T> 優先於Comparable<T>。

        修改後的方法中則只需要將Iterator<T> i = list.iterator()改爲Iterator<? extends T> i = list.iterator()。
        總而言之,通配符類型雖然比較需要技巧,但是使API變得靈活。如果編寫的是將被廣泛使用的類庫,則一定要適當地利用通配符類型。記住基本的原則:producer-extends, consumer-super(PECS)。還要記住所有的Comparator和Comparable都是消費者。


二十九、優先考慮類型安全的異構容器

        泛型通常用於集合,如Set和Map等。這樣的用法也就限制了每個容器只能有固定數目的類型參數,一般來說,這也確實是我們想要的。然而有的時候我們需要更多的靈活性,如數據庫可以用任意多的Column,如果能以類型安全的方式訪問所有Columns就好了,幸運的是有一種方法可以很容易的做到這一點,就是將key進行參數化,而不是將容器參數化,見以下代碼:

public class Favorites {
    public <T> void putFavorite(Class<T> type,T instance);
    public <T> T getFavorite(Class<T> type);
}
        下面是該類的使用示例:
public static void main(String[] args) {
    Favorites f = new Favorites();
    f.putFavorite(String.class,"Java");
    f.putFavorite(Integer.class,0xcafebabe);
    f.putFavorite(Class.class,Favorites.class);
    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);
    System.out.printf("%s %x %s\n",favoriteString,favoriteInteger,favoriteClass.getName());
}
//Java cafebabe Favorites

        這裏Favorites實例是類型安全的:當你請求String的時候,它不會給你Integer。同時它也是異構:不像普通的Map,他的所有鍵都是不同類型的。因此,我們將Favorites稱作類型安全的異構容器。下面就是Favorites的具體實現:

public class Favorites {
    private Map<Class<?>,Object> favorites = new HashMap<Class<?>,Object>();

    public <T> void putFavorite(Class<T> type,T instance) {
        if (type == null)
            throw new NullPointerException("Type is null");
    favorites.put(type,type.cast(instance));
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

        可以看出每個Favorites實例都得到一個Map<Class<?>,Object>容器的支持,每個鍵都可以有一個不同的參數化類型,異構就是從這裏來的。由於該容器的值類型爲Object,即Map不能保證每個值的類型都與鍵的類型相同。爲了進一步確實類型的安全性,我們在put的時候通過Class.cast()方法將Object參數嘗試轉換爲Class所表示的類型,如果類型不匹配,將會拋出ClassCastException異常。以此同時,在從Map中取出值對象的時候,由於該對象當前的類型是Object,因此我們需要再次利用Class.cast()函數將其轉換爲我們的目標類型。

        對於Favorites類的put/get方法,有一個非常明顯的限制,即我們無法將“不可具體化”類型存入到該異構容器中,如List<String>、List<Integer>等泛型類型。這樣的限制主要源於Java中泛型類型在運行時的類型擦除機制,即List<String>.class和List<Integer>.class是等同的對象,均爲List.class。如果Java編譯器通過了這樣的調用代碼,那麼List<String>.class和List<Integer>.class將會返回相同的對象引用,從而破壞Favorites的內部結構。
        總而言之,集合API說明了泛型的一般用法,限制你每個容器只能有固定數目的類型參數。你可以通過將類型參數放在鍵上而不是容器上來避開這一限制。對於這種類型安全的異構容器,可以用Class對象作爲鍵。以這種方式使用的Class對象稱作類型令牌。你也可以使用定製的鍵的類型。例如,用一個DatabaseRow類型表示一個數據庫行(容器),用泛型Column<T>作爲它的鍵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章