Effective Java 第三版——31.使用限定萬用字元來增加API的靈活性

來源:互聯網
上載者:User

標籤:傳遞   圖片   生產   ges   choices   沒有   解決方案   導致   scheduled   

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

31. 使用限定萬用字元來增加API的靈活性

如條目 28所述,參數化型別是不變的。換句話說,對於任何兩個不同類型的Type1和Type,List <Type1>既不是List <Type2>子類型也不是其父類型。儘管List <String>不是List <Object>的子類型是違反直覺的,但它確實是有道理的。 可以將任何對象放入List <Object>中,但是只能將字串放入List <String>中。 由於List <String>不能做List <Object>所能做的所有事情,所以它不是一個子類型(條目 10 中的裡氏替代原則)。

相對於提供的不可變的類型,有時你需要比此更多的靈活性。 考慮條目 29中的Stack類。下面是它的公用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);}

這種方法可以乾淨地編譯,但不完全令人滿意。 如果可遍曆的src元素類型與棧的元素類型完全符合,那麼它工作正常。 但是,假設有一個Stack <Number>,並調用push(intVal),其中intVal的類型是Integer。 這是因為Integer是Number的子類型。 從邏輯上看,這似乎也應該起作用:

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

但是,如果你嘗試了,會得到這個錯誤訊息,因為參數化型別是不變的:

StackTest.java:7: error: incompatible types: Iterable<Integer>cannot be converted to Iterable<Number>        numberStack.pushAll(integers);                            ^

幸運的是,有對應的解決方案。 該語言提供了一種特殊的參數化型別來調用一個限定萬用字元類型來處理這種情況。 pushAll的輸入參數的類型不應該是“E的Iterable介面”,而應該是“E的某個子類型的Iterable介面”,並且有一個萬用字元類型,這意味著:Iterable <? extends E>。 (關鍵字extends的使用有點誤導:回憶條目 29中,子類型被定義為每個類型都是它自己的子類型,即使它本身沒有繼承。)讓我們修改pushAll來使用這個類型:

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

有了這個改變,Stack類不僅可以乾淨地編譯,而且用戶端代碼也不會用原始的pushAll聲明編譯。 因為Stack和它的用戶端乾淨地編譯,你知道一切都是型別安全的。

現在假設你想寫一個popAll方法,與pushAll方法相對應。 popAll方法從棧中彈出每個元素並將元素添加到給定的集合中。 以下是第一次嘗試編寫popAll方法的過程:

// popAll method without wildcard 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);

如果嘗試將此用戶端代碼與之前顯示的popAll版本進行編譯,則會得到與我們的第一版pushAll非常類似的錯誤:Collection <Object>不是Collection <Number>的子類型。 萬用字元類型再一次提供了一條出路。 popAll的輸入參數的類型不應該是“E的集合”,而應該是“E的某個父類型的集合”(其中父類型被定義為E是它自己的父類型[JLS,4.10])。 再次,有一個萬用字元類型,正是這個意思:Collection <? super E>。 讓我們修改popAll來使用它:

// Wildcard type for parameter that serves as an E consumerpublic void popAll(Collection<? super E> dst) {    while (!isEmpty())        dst.add(pop());}

通過這個改動,Stack類和用戶端代碼都可以乾淨地編譯。

這個結論很清楚。 為了獲得最大的靈活性,對代表生產者或消費者的輸入參數使用萬用字元類型。 如果一個輸入參數既是一個生產者又是一個消費者,那麼萬用字元類型對你沒有好處:你需要一個精確的類型匹配,這就是沒有任何萬用字元的情況。

這裡有一個助記符來協助你記住使用哪種萬用字元類型:
PECS代表: producer-extends,consumer-super。

換句話說,如果一個參數化型別代表一個T生產者,使用<? extends T>;如果它代表T消費者,則使用<? super T>。 在我們的Stack樣本中,pushAll方法的src參數產生棧使用的E執行個體,因此src的合適類型為Iterable<? extends E>;popAll方法的dst參數消費Stack中的E執行個體,因此dst的合適類型是Collection <? super E>。 PECS助記符抓住了使用萬用字元類型的基本原則。 Naftalin和Wadler稱之為擷取和放置原則( Get and Put Principle )[Naftalin07,2.4]。

記住這個助記符之後,讓我們來看看本章中以前項目的一些方法和構造方法聲明。 條目 28中的Chooser類構造方法有這樣的聲明:

public Chooser(Collection<T> choices)

這個構造方法只使用集合選擇來生產類型T的值(並將它們儲存起來以備後用),所以它的聲明應該使用一個extends T的萬用字元類型。下面是得到的構造方法聲明:

// Wildcard type for parameter that serves as an T producerpublic Chooser(Collection<? extends T> choices)

這種改變在實踐中會有什麼不同嗎? 是的,會有不同。 假你有一個List <Integer>,並且想把它傳遞給Chooser<Number>的構造方法。 這不會與原始聲明一起編譯,但是它只會將限定萬用字元類型添加到聲明中。

現在看看條目 30中的union方法。下是聲明:

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

兩個參數s1和s2都是E的生產者,所以PECS助記符告訴我們該聲明應該如下:

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

請注意,傳回型別仍然是Set <E>。 不要使用限定萬用字元類型作為傳回型別。除了會為使用者提供額外的靈活性,還強制他們在用戶端代碼中使用萬用字元類型。 通過修改後的聲明,此代碼將清晰地編譯:

Set<Integer>  integers =  Set.of(1, 3, 5);Set<Double>   doubles  =  Set.of(2.0, 4.0, 6.0);Set<Number>   numbers  =  union(integers, doubles);

如果使用得當,類的使用者幾乎不會看到萬用字元類型。 他們使方法接受他們應該接受的參數,拒絕他們應該拒絕的參數。 如果一個類的使用者必須考慮萬用字元類型,那麼它的API可能有問題。

在Java 8之前,類型推斷規則不夠聰明,無法處理先前的程式碼片段,這要求編譯器使用上下文指定的傳回型別(或目標類型)來推斷E的類型。union方法調用的目標類型如前所示是Set <Number>。 如果嘗試在早期版本的Java中編譯片段(以及適合的Set.of工廠替代版本),將會看到如此長的錯綜複雜的錯誤訊息:

Union.java:14: error: incompatible types        Set<Number> numbers = union(integers, doubles);                                   ^  required: Set<Number>  found:    Set<INT#1>  where INT#1,INT#2 are intersection types:    INT#1 extends Number,Comparable<? extends INT#2>    INT#2 extends Number,Comparable<?>

幸運的是有辦法來處理這種錯誤。 如果編譯器不能推斷出正確的類型,你可以隨時告訴它使用什麼類型的顯式型別參數[JLS,15.12]。 甚至在Java 8中引入目標類型之前,這不是你必須經常做的事情,這很好,因為顯式型別參數不是很漂亮。 通過添加顯式型別參數,如下所示,程式碼片段在Java 8之前的版本中進行了乾淨編譯:

// Explicit type parameter - required prior to Java 8Set<Number> numbers = Union.<Number>union(integers, doubles);

接下來讓我們把注意力轉向條目 30中的max方法。這裡是原始聲明:

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

以下是使用萬用字元類型的修改後的聲明:

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

為了從原來到修改後的聲明,我們兩次應用了PECS。首先直接的應用是參數列表。 它產生T執行個體,所以將類型從List <T>更改為List<? extends T>。 棘手的應用是型別參數T。這是我們第一次看到萬用字元應用於型別參數。 最初,T被指定為繼承Comparable <T>,但Comparable的T消費T執行個體(並產生指示循序關聯性的整數)。 因此,參數化型別Comparable <T>被替換為限定萬用字元類型Comparable<? super T>。 Comparable執行個體總是消費者,所以通常應該使用Comparable<? super T>優於Comparable <T>。 Comparator也是如此。因此,通常應該使用Comparator<? super T>優於Comparator<T>。

修改後的max聲明可能是本書中最複雜的方法聲明。 增加的複雜性是否真的起作用了嗎? 同樣,它的確如此。 這是一個列表的簡單例子,它被原始聲明排除,但在被修改後的版本裡是允許的:

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

無法將原始方法聲明應用於此列表的原因是ScheduledFuture不實現Comparable <ScheduledFuture>。 相反,它是Delayed的子介面,它繼承了Comparable <Delayed>。 換句話說,一個ScheduledFuture執行個體不僅僅和其他的ScheduledFuture執行個體相比較: 它可以與任何Delayed執行個體比較,並且足以導致原始的聲明拒絕它。 更普遍地說,萬用字元要求來支援沒有直接實現Comparable(或Comparator)的類型,但繼承了一個類型。

還有一個關於萬用字元相關的話題。 型別參數和萬用字元之間具有雙重性,許多方法可以用一個或另一個聲明。 例如,下面是兩個可能的聲明,用於交換列表中兩個索引項目的靜態方法。 第一個使用無限制型別參數(條目 30),第二個使用無限制萬用字元:

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

這兩個聲明中的哪一個更可取,為什嗎? 在公用API中,第二個更好,因為它更簡單。 你傳入一個列表(任何列表),該方法交換索引的元素。 沒有型別參數需要擔心。 通常,如果型別參數在方法聲明中只出現一次,請將其替換為萬用字元。 如果它是一個無限制的型別參數,請將其替換為無限制的萬用字元; 如果它是一個限定型別參數,則用限定萬用字元替換它。

第二個swap方法聲明有一個問題。 這個簡單的實現不會編譯:

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

試圖編譯它會產生這個不太有用的錯誤資訊:

Swap.java:5: error: incompatible types: Object cannot beconverted to CAP#1        list.set(i, list.set(j, list.get(i)));                                        ^  where CAP#1 is a fresh type-variable:    CAP#1 extends Object from capture of ?

看起來我們不能把一個元素放回到我們剛剛拿出來的列表中。 問題是列表的類型是List <?>,並且不能將除null外的任何值放入List <?>中。 幸運的是,有一種方法可以在不使用不安全的轉換或原始類型的情況下實現此方法。 這個想法是寫一個私人輔助方法來捕捉萬用字元類型。 輔助方法必須是泛型方法才能捕獲類型。 以下是它的定義:

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

swapHelper方法知道該列表是一個List <E>。 因此,它知道從這個列表中獲得的任何值都是E類型,並且可以安全地將任何類型的E值放入列表中。 這個稍微複雜的swap的實現可以乾淨地編譯。 它允許我們匯出基於萬用字元的漂亮聲明,同時利用內部更複雜的泛型方法。 swap方法的用戶端不需要面對更複雜的swapHelper聲明,但他們從中受益。 輔助方法具有我們認為對公用方法來說過於複雜的簽名。

總之,在你的API中使用萬用字元類型,雖然棘手,但使得API更加靈活。 如果編寫一個將被廣泛使用的類庫,正確使用萬用字元類型應該被認為是強制性的。 記住基本規則: producer-extends, consumer-super(PECS)。 還要記住,所有Comparable和Comparator都是消費者。

Effective Java 第三版——31.使用限定萬用字元來增加API的靈活性

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.