1. 程式人生 > >Effective Java 泛型 第28條:利用有限制萬用字元來提升API的靈活性

Effective Java 泛型 第28條:利用有限制萬用字元來提升API的靈活性

如第25條所述,引數化型別是 不可變的(invariant)。換句話說,對於任何兩個截然不同的型別tyle1和type2來說,List< Type1>既不是List< Type2>的子型別,也不是他的超型別。雖然List< String>不是List< Object>的子型別,這與直覺相悖,但是實際上很有意義。你可以將任何物件放進一個List< Object>中,卻只能將字串放進< String>中。

有時候,我需要的靈活性要比不可變型別所能提供的更多。考慮第26條中的堆疊下面就是他的公共API:

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

假設我們想要增加一個方法,讓她按照順序將一系列的元素全部放到堆疊中。這是第一次嘗試,如下:

// pushAll method without wildcard type - deficient;  
public void pushAll(Iterable<E> src) {  
    for (E e: src)  
        push(e);  
}  

這個方法編譯的時候正確無誤,但是並非盡如人意,如果Iterable src的元素型別與堆疊的完全匹配,那就沒有問題,但是假如有一個Stack< Number>,並且呼叫了push(intVal),這裡的intVal就是Integer型別。這是可以的,因為Integer是Number的一個子型別,因此從邏輯上來說,下面這斷程式碼應該是可行的:

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

但是實際上執行這段程式碼會得到下面的錯誤,因為如前所述,引數化型別是不可變的:

StackTest.java:7:pushAll(Iterable<Number>) in Stack<Number>
cannot be applied to (Iterable<Inteage>)
	numberStack.pushAll(integers);

幸運的是Java提供了一種解決方法,稱為有限制的萬用字元型別,來處理類似的情況。pushALL的輸入引數型別不應該為"E的Iterable介面",而應該為"E的某個子型別的Iterable介面",有一個萬用字元型別正符合此意:Iterable<? extends E>,修改pushAll來使用這個型別。

// Wildcard type for parameter that serves as an E producer  
public void pushAll(Iterable<? extends E> src) {  
    for (E e: src)  
        push(e);  
} 

這麼修改了之後,不僅Stack可以正確無誤的編輯,沒有通過初試的pushAll宣告進行編譯的客戶端程式碼也一樣可以,因為Stack及其客戶端正無誤的進行了編譯,你就知道一切都是型別安全的了。

現在假設想要編寫一個pushAll方法,使之與popAll方法相呼應。popAll方法從堆疊中彈出每個元素,並將這些元素新增到指定的集合中。初次嘗試編寫的popAll方法可能像下面這樣:

// popAll method without wildcart type - deficient;  
public void popAll(Collection<E> dst) {  
    while(!isEmpty())  
        dst.add(pop());  
}  

如果目標集合的元素型別與堆疊完全匹配,這段程式碼編譯時還是會正確無誤的。執行得很好,但是,也並不意味著盡如人意。假設你有一個Stack< Number>和型別Object變數,如果從堆疊中彈出一個元素,並將它儲存在該變數中,它的編譯和執行都不會出錯,考慮如下程式碼 :

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

仍然有一個萬用字元型別正式符合此意:Collection<? super E>.

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

結論很明顯,為了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入引數上使用萬用字元型別。如果某個輸入引數既是生產者,又是消費者,那麼萬用字元型別就沒有什麼好處了,因為需要的是嚴格的型別匹配,這是不用任何萬用字元而得到的。

下面的助記符便於讓你記住要使用哪種萬用字元型別型別: PESC表示producter-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這個助記符突出了使用萬用字元型別的基本原則。Naftalin和wadler稱之為Get and Put Principle。 第25條中的reduce方法就有這條宣告:

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

雖然列表既可以消費也可以是產生值,reduce方法還是隻用他的list引數作為E生產者,因此他的宣告就應該使用一個extends E得萬用字元型別。引數f表示既可以消費又可以產生E例項的函式,因此萬用字元型別不適合他,得到的方法宣告如下:

static <E> E reduce(List<? extends E> list, Function<E> f, E initVal);

區別所在:假設有一個List< Integer>,想通過Function< Number>把他簡化。他不能通過初始宣告進行編譯,但是一旦添加了有限制的萬用字元型別就可以了。

27條中的union方法:

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

注意返回的型別仍然是Set< E>。不要用萬用字元型別作為返回型別。除了為使用者提供額外的靈活性之外,他還會強制使用者在客服端程式碼中使用統配符型別。

如果使用得當,萬用字元型別對於類的使用者來說幾乎是無形的。他們使方法能夠接受他們應該接受的引數,並拒絕那些應該拒絕的引數。如果類的使用者必須考慮萬用字元型別,類的API或許就會出錯。 遺憾的是,型別推導規則相當複雜。而且他們並非總能完成需要他們完成的工作。看看修改過的union宣告,你可能會以為可以向這樣編寫:

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

但這麼做會得到下面的錯誤訊息“

Union.java:14: incompatible types
found : Set<Number & Comparable<? extends Number & Comparable<?>>>
required:Set<Number>
		Set<Number> numbers=union(intVal,deficient);

使用顯示的型別引數來告訴他要使用的型別,可以正確無誤的編譯:

	Set<Number> numbers=Unioc.<Number>union(intVal,deficient);

接下來,再看看27條的max方法:

public static <T extends Comparable<T>> T max(List<T> list)

修改過後:

public static <T extends Comparable<? super T>> T max(List<T> list)

下面是一個簡單的列表例項,在初始的宣告中不允許這樣,修改過的版本則可以:

List<ScheduledFuture<?>> scheduledFutures =...;

在初始的宣告中不允許這樣? 因為java.util.concurrent.ScheduledFuture沒有實現Comparable介面。相反,他是擴充套件Comparable介面的Delayed介面的子介面。換句話說,ScheduleFuture例項並非只能與其他ScheduledFuture例項相比較;他可以與任何Delayed例項想比較,這就足以導致初始宣告時會被拒絕。

修改過的max宣告有一個小小的問題:它阻止方法進行編譯。羨慕的方法包含了修改過的宣告:

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

以下是他編譯時會產生的錯誤:

Max.java:7: incompatible types
found : Iterator<capture#591 of extends T>
required:Iterator<T>
	Iterator<T> i=list.iterator();

這條錯誤訊息意味著list不是一個List< T>,因此它的iterator方法沒有返回Iterator< T>,它返回T的某個子型別的一個iterator,因此我們用他代替iterator宣告,它使用一個有限責任的萬用字元型別:

Iterator<? extends T> i=list.iterator();

這是必須對方法體所做的唯一修改。迭代器的next方法放回的元素屬於T的某個子型別,因此他們可以被安全的儲存在型別T的一個變數中。

還有一個與萬用字元有關的話題值得探討。型別引數和萬用字元之間具有雙重性,許多方法都可以利用其中一個或者另一個進行宣告。例如,下面是可能的兩種靜態方法宣告,來交換列表中的兩個被索引的專案。第一個使用無限制的型別引數,第二個使用無限制的萬用字元:

// Two possible declarations for the swap method  
public static <E> void swap(List<E> list, int i, int j);  
public static void swap(List<?> list, int i, int j);  

第二種方法更好一些,因為它更簡單。將它傳到一個列表中——任何列表——方法就會交換被索引的元素。不用擔心型別引數。一般來說,如果型別引數只在方法宣告中出現一次,就可以用萬用字元取代它。如果是無限制的型別引數就用無限制的萬用字元來取代,如果 是有限制的型別引數,就用有限制的萬用字元來取代。

第二種方法存在一個問題,它優先使用萬用字元而非型別引數,下面的簡單實現都實現不了:

public static void swap(List<?> list, int i, int j) {  
    list.set(i, list.set(j, list.get(i)));  
} 

編譯時產生的錯誤:

Swap.java:5 set(int.capture#282 of ?) in List<capture#282 of ?>
cannot be applied to (int,Object)
	list.set(i,list.set(j,list.get(i)));

不能將元素放回到剛剛從中取出的列表中?原來問題在於list的型別為List<?>,你不能把null之外的任何值放到List<?>。幸運的是,有一種方式可以實現這種方法,無需求助不安全的轉換或者原始型別。這種想法 就是編寫一個私有輔助方法來捕捉萬用字元型別。為了捕捉型別,輔助方法必須是泛型方法,像下面一樣:

public static void swap(List<?> list, int i, int j) {  
    swapHelper(list, i, j);  
}  
  
// Private helper method for wildcard capture  
private static <E> void swapHelper(List<E> list, int i, int j) {  
    list.set(i, list.set(j, list.get(i)));  
}  

swapHelper方法知道list是List它知道從這個列表中取出的任何值均為E型別,並知道將E型別的任何值放進列表都是安全的。

總而言之,在API中使用萬用字元型別雖然比較需要技巧,但是使API變得靈活得多,如果編寫的是將被廣泛使用的類庫,則一定要適當地利用萬用字元型別,記住基本的原則:producer-extends,consumer-super(PECS),還要記得所有的comparable和comparator都是消費者。