天天看點

Effective Java(四)四、泛型

四、泛型

1. 請不要再新代碼中使用原生态類型

        泛型類/接口:聲明中具有一個或者多個類型參數的類/接口。

        每個泛型都定義一個原生态類型,即不帶任何實際類型參數的泛型名稱。如與List<E>相對應的原生态類型就是List。原生态類型沒有泛型在安全性和表述性方面的優勢,它的存在僅是為了相容引入泛型之前的遺留代碼,不應在新代碼中繼續使用。

//使用原生态類型
private final List stamps = new ArrayList();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //可以正常添加
Stamp stamp = (Stamp)stamps.get(1); //運作時錯誤,抛出ClassCastException。

//使用泛型
private final List<Stamp> stamps = new ArrayList<Stamp>();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //提示錯誤,無法通過編譯
Stamp stamp = stamps.get(0); //使用時無需進行手工轉換
           

由上述代碼可以看出,使用泛型的兩個好處:

  • 由編譯器確定插入正确的元素類型
  • 從集合擷取元素時不再需要手工轉換了 

        如果要使用泛型,但不确定或不關心實際的類型參數,可以使用一個?代替,稱作無限制的通配符類型,如泛型Set<E>的無限制通配符類型為Set<?>,讀作某個類型的集合。通配符類型是安全的,原生态類型不安全。

不在新代碼中使用原生态類型這條規則有兩種例外情況:

(1)在類文字中必須使用原生态類型

//正确的用法
List.class
String[].class
int.class

//錯誤的用法
List<String.class>
List<?>.class
           

(2)在instanceof操作符中必須使用原生态類型 

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

        上述兩種例外都是源于泛型資訊可以在運作時被擦除。 

2. 消除非受檢警告

        用泛型程式設計時,會遇到很多編譯器警告:

  • 非受檢強制轉換警告
  • 非受檢方法調用警告
  • 非受檢普通數組建立警告
  • 非受檢轉換警告

        要盡可能地消除每一個非受檢警告,這可以確定代碼是類型安全的,意味着代碼在運作時不會出現ClassCastException異常。

        如果無法消除警告,同時可以證明引起警告的代碼是類型安全的。隻有在這種條件下才可以使用 @SuppressWarnings("unchecked") 注解來禁止這種警告。

        SuppressWarnings注解可以用在任何粒度的級别中,從單獨的局部變量聲明到整個類的定義都可以。應該始終在盡可能小的範圍中使用SuppressWarnings注解,永遠不要在整個類上使用SuppressWarnings,因為這麼做可能會掩蓋重要的警告資訊。

        每當使用@SuppressWarnings("unchecked")注解時,都要添加一條注釋,說明為什麼這麼做是安全的。這樣做可以幫助他人了解代碼,更重要的是,可以盡量減少其他人修改代碼後導緻計算不安全的機率。

3. 清單優先于數組

數組與泛型相比,有兩個重要的不同點:

(1)數組是協變的

        協變指的是如果Sub為Super的子類型,那麼數組類型Sub[]就是Super[]的子類型;

        泛型是不可變的,對于任意兩個不同的類型Type1和Type2,List<Type1>既不是List<Type2>的子類型,也不是List<Type2>的超類型。

(2)數組是具體化的

        數組在運作時才知道并檢查它們的元素類型限制;

        泛型則通過類型擦除來實作,它在編譯時強化它們的類型資訊,在運作時丢棄(或擦除)它們的元素類型資訊。

        由于數組的協變性和具體化,它是有缺陷的:

//數組具有協變性,Object是Long的父類,聲明合法
Object[] objectArray = new Long[1];
//Long[] 退化為Object[],此處指派也是合法的
objectArray[0] = "I don't fit in"; 
           

        上述代碼可以通過編譯,但運作時卻抛出ArrayStoreException。

        改為清單後,則無法通過編譯時的類型檢查: 

//無法通過編譯,List<Object>和List<Long>是不同的類型
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");
           

        因為數組和泛型之間有着根本性的差別,數組和泛型不能很好地混合使用。如下列類型的表達式都是非法的:new List<E>[]、new List<String>[]、new E[]。

        建立泛型數組是非法的,是因為泛型數組不是類型安全的。如下代碼所示:

List<String>[] strLists = new List<String>[1]; //假設此處合法
List<Integer> intList = Arrays.asList(42);
Object[] objects = strLists; //數組是協變的,此處合法
objects[0] = intList;
String s = strLists[0].get(0); //運作時ClassCastException異常
           

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

4.優先考慮泛型

        編寫自己的泛型相對比較困難,但很值得花時間去學習如何編寫。

        下面以一個Stack類為例來說明:

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 * DEFAULT_INITIAL_CAPACITY + 1);
        }
    }
}
           

        上述Stack類的實作,主要問題有如下兩點:

(1)push操作無法保證類型安全

//可以向stack中放入任意類型
Stack stack = new Stack();
stack.push("stack");
stack.put(new Integer(100));
           

(2)pop操作獲得元素需要外部手工進行類型轉換,且可能會産生ClassCastException異常。

String str = (String)stack.pop();
           

将上述Stack類進行泛型化,主要步驟為:

  • 給它的聲明添加一個或者多個類型參數
  • 用相應的類型參數替換所有的Object類型,嘗試編譯
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 * DEFAULT_INITIAL_CAPACITY + 1);
        }
    }
}
           

消除泛型數組的方法有兩種:

(1)直接繞過建立泛型數組,建立一個Object數組

//用法合法,但整體上而言不是類型安全的
elements = (E[])Object[DEFAULT_INITIAL_CAPACITY];
           

(2)将域的類型從E[]改為Object[](推薦使用此種方法)

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

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = (E) elements[--size];
        elements[size] = null;
        return result;
    }
}
           

5. 優先考慮泛型方法

        靜态工具方法通常比較适合泛型化。

        編寫泛型方法與編寫泛型類相似,如下述代碼:

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

        上述union方法并不是類型安全的,将其泛型化的代碼如下:

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

        泛型後的union方法不僅适用性更強,也是類型安全的,它確定了待合并集合的類型一緻性,外部使用也無需進行手工強制轉換。

6. 利用有限制通配符來提升API的靈活性

        泛型不具備協變性,但有時,我們又需要使用協變帶來的靈活性,于是Java提供了有限制的通配符類型這種特殊的參數化類型:

  • GenericType<? extends E>:子類型通配符,通配符?表示E的某個子類型
  • GenericType<? super E>:超類型通配符,通配符?表示E的某個超類型

        考慮Stack的公共API:

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

        假如我們想增加一個方法,讓它按順序把一系列元素添加到Stack中,嘗試如下:

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

        如果src中的元素類型與Stack的泛型參數類型完全比對,是完全沒有問題的。但考慮這樣一種情形:有一個Stack<Number>,且調用了push(int val),從邏輯上講,下面的實作應該是可以的:

Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
           

        實際情況是上述辦法并不可行,會導緻編譯錯誤。

        顯然,我們的目的是想将E的某個子類型也放入Stack中(Integer是Number的子類),可以利用子類型通配符來做有限制的規定:

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

        假設現在需要編寫一個popAll方法,使之與pushAll方法相呼應,popAll方法從Stack中彈出每個元素,并将這些元素添加到指定的集合中,嘗試如下:

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

        如果dst的元素類型與Stack完全比對,上述實作是沒有問題的。但考慮這樣一種情形:有一個Stack<Number>和Collection<Object>,從邏輯上講,下面的實作應該是可以的:

Stack<Number> numStack = new Stack<Number>();
Collection<Object> coll = ...;
numStack.popAll(coll);
           

        實際情況是上述辦法并不可行,會導緻編譯錯誤。Collection<Object>并不是Collection<Number>的超類型。

我們的目的是為了将類型為E的元素加入到目标泛型集合中,且目标集合的泛型參數類型隻要是類型E的父類型即可(Object是Number的父類),Java提供了父類型通配符來實作這種需求:

//此處的限定是:通配符類型是泛型參數類型的父類即可
public void popAll(Collection<? super E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}
           

        為了便于記住要使用哪種通配符,引入下面的助記符:

        PECS表示producer-extends,consumer-super。

        如果參數化類型表示一個T生産者,就使用<? extends T>;如果它表示一個T消費者,就使用<? super T>。在Stack示例中,pushAll的src參數産生E執行個體供Stack使用,是以src相應的類型為Iterable<? extends E>;popAll的dst參數通過Stack消費E執行個體,是以dst相應的類型為Collection<? super E>。

        PECS助記符突出了使用通配符類型的基本原則。

7. 優先考慮類型安全的異構容器

        Java泛型也提供了另一種用法:将鍵(key)進行參數化而不是将容器參數化,然後将參數化的鍵送出給容器,來插入或者擷取值,用泛型系統來確定值的類型與它的鍵相符。

        類Class在Java1.5中被泛化了,類的類型從字面上看不再隻是簡單的Class,而是Class<T>,意味着String.class是屬于Class<String>類型,Integer.class屬于Class<Integer>類型。

        當一個類的字面文字被用在方法中,來傳達編譯時和運作時的類型資訊時,被稱作type token。

        假如需要設計一個Favorites類,它允許其用戶端從任意數量的其他類中,儲存并獲得一個“最喜愛”的執行個體,代碼如下:

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

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

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorities.get(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 fString = f.getFavorite(String.class);
        int fInteger = f.getFavorite(Integer.class);
        Class<?> fClass = f.getFavorite(Class.class);

        System.out.printf("%s %x %s%n", fString, fInteger, fClass.getSimpleName());
    }
}
//代碼列印結果為:Java cafebabe Favorites
           

        Favorites執行個體是類型安全的:當你向它請求String的時候,它不會傳回一個Integer。同時它也是異構的:不像普通的map,它的所有鍵都是不同類型的。

        像Favorites這種類被稱為類型安全的異構容器。對于這種類型安全的異構容器,可以用Class對象作為鍵。

        以這種方式使用的Class對象被稱作類型令牌。Favorites使用的類型令牌是無限制的,還可以利用有限制類型參數或有限制通配符來限制可以表示的類型:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

    Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
        Class<?> annotationType = null;
        try {
            annotationType = Class.forName(annotationTypeName);
        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
        return element.getAnnotation(annotationType.asSubclass(Annotation.class));
    }
           

繼續閱讀