Effective Java(3rd)-Item31 使用有界通配符提高API的靈活性

??正如item28所指出得,參數(shù)化類型是不變的。還擊話說,對于type1和type2這兩種不同的類型,List<Type1>既不是List<Type2>的子類型也不是父類型。雖然List<String>不是List<Object>的子類型是反直覺的,但是它確實是有意義的。你可以將任何對象放入List<Object>,但是你只能將String類型放入List<String>,由于List<String>不能完成List<Object>的所有操作,所以它不是子類型(里氏代換原則,item10).
??有時候,你需要比不變量類型所能提供的更多的靈活性??紤]Item29的Stack類。為了刷新你的內存,這是它的公有API:

image.png

??假設我們要添加一個方法,該方法接受一系列元素并將他們全部推到Stack上。如下是第一次嘗試:


image.png

??這種方法編譯清晰,但并不完全令人滿意。如果迭代器src的元素類型與Stack的類型匹配,它能正常工作。但是假設你有Stack<Number>,并且你調用了push(intVal),intVal是Integer類型。這能正常工作因為Integer是Number的子類。所以邏輯上,它看起來這也應該正常工作:


image.png

??如果你這么嘗試了,你會得到如下錯誤信息,因為參數(shù)化類型是不可變的:


image.png

??幸運的是,有一條出路。語言提供了一種特殊的參數(shù)化類型調用-有界通配符類型,以處理這種情況。輸入?yún)?shù)的類型應該不是“Iterable of E”而是“Iterable of some subtype of E”.有一種通配符類型,它的確切意思是:Iterable<? extends E>.(extends關鍵字的使用是略微誤導了:回想一下item29,即定義了子類型,以便每個類型都是自身的一個子類型,即使它不擴展自己),讓我們一起修改pushAll來使用這個類型:


image.png

??隨著此更改,Stack不僅可以干凈地編譯,而且不會使用原來的pushAll聲明編譯的客戶端代碼也一樣。因為Stack及其客戶端編譯干凈,所以你知道所有的東西都是類型安全的。
??現(xiàn)在假設你想要編寫一個popAll方法與pushAll類似。popAll方法彈出Stack的每個元素,并將元素添加到給定的集合中。這是如何編寫popAll方法的初次嘗試的樣子:


image.png

??同樣,編譯干凈切工作良好,如果目標集合的元素類型與Stack的元素類型完全匹配。但是同樣,這并不完全令人滿意。假設你有Stack<Number>和Object類型的變量。如果你從stack中pop一個元素并在變量中存儲,它編譯通過并運行沒有錯誤。所以你不能這么做嗎?


image.png

??如果你嘗試根據(jù)前面顯示的popAll版本編譯該客戶端代碼,你將得到一個非常類似我們使用第一個版本的pushAll:Collection<Object>不是Collection<Number>的子類。再一次,通配符類型提供了一條出路。popAll的輸入?yún)?shù)應該不是”collection of E“,而是”collection of some supertype of E“.(其中,超類中有定義為本身即本身的超類 [JLS, 4.10])。同時,有一個通配符類型,它精確地標識: Collection<? super E>.讓我們修改popAll方法來使用:


image.png

??隨著這個改變,Stack和客戶端代碼編譯都很清晰。
??這個教訓很清晰。為了最大的靈活性,在輸入?yún)?shù)上使用通配符類型代表了生產者或消費者。如果一個輸入?yún)?shù)都是生產者和一個消費者,那么通配符類型對你沒有好處:你需要一個精確的類型匹配,這就是沒有任何通配符的情況。
??下面是幫助你記住使用哪種通配符類型的助記符:
PECS stands for producer-extends, consumer-super
??換句話說,如果一個參數(shù)化類型代表了T生產者,使用<? extends T>;如果代表了一個T 消費者,使用<? super T>.在我們的Stack 例子中,通過Stack的pushAll的src參數(shù)生產者E實例的使用,所以src的合適的類型是Iterable<? extends E>;popAll的dst參數(shù)從stack消費E實例,所以dst的合適的類型是Collection<? super E>.PESC助記符捕獲了指導通配符類型使用的基本原則。Naftalin和Wadler稱其為GET和PUT原則 [Naftalin07, 2.4]。
??隨著內心有著這個助記符,讓我們看看在本章前幾項中的一些方法和構造方法聲明。在item28中的Chooser構造方法有如下聲明:

image.png

??這個構造方法只使用集合選項來生成T類型的值(并存儲它們供以后使用),因此它的聲明應該使用擴展T的通配符類型。下面是生成的構造方法聲明:


image.png

??這種變化在實際上有改變嗎?是的,它有。假設你有一個List<Integer>,你希望將它傳遞給Chooser<Numer>的構造方法。這將不會使用原始聲明進行編譯,但一旦將有界通配符類型添加到聲明中,則會進行編譯。
??現(xiàn)在,讓我們一起從Item30中看union方法。這是聲明:


image.png

??s1和s2和生產者E,根據(jù)PECS助記告訴我們聲明應該是這樣的:


image.png

??注意到返回類型仍然是Set<E>。不要將有界通配符作為返回類型。它將迫使用戶在客戶端代碼中使用通配符類型,而不是為用戶提供額外的靈活性。使用修改吼的聲明,該代碼將干凈地編譯:

image.png

??如果使用得當,類的用戶幾乎看不到通配符類型。它們導致方法接受它們應該接受的參數(shù),并拒絕它們應該拒絕的參數(shù)。如果類的用戶不得不考慮通配符類型,可能是API出錯了。
??在Java8之前,類型推斷規(guī)則并不足夠智能,無法處理前面的代碼片段,這要求編譯器使用上限為指定的返回類型(或目標類型)來推斷E的類型。前面顯示的聯(lián)合調用的目標類型是Set<Number>.如果你嘗試在早期版本的Java中編譯該片段(對工廠Set.of進行適當?shù)奶鎿Q),你將得到一條冗長,復雜的錯誤消息,如下所示:

image.png

??幸運的是,有一種方法可以處理這類錯誤。如果編譯器不能推斷正確的類型,你可以總是告訴它在顯示類型中使用哪種類型 [JLS, 15.12]。即使在Java8中引入目標類型之前,這也不是你必須經常做的事情,這很好,因為顯式類型參數(shù)不是很漂亮。通過添加顯式類型參數(shù)(如此處所示),代碼片段在Java8之前的版本中可以清晰地編譯:


image.png

??接下來,讓我們關注item30的max方法,這是最初的聲明:


image.png

??這是使用通配符類型修改的聲明:


image.png

??為了得到修改后的聲明,我們使用了兩次PECS的啟發(fā)。簡單的應用程序是參數(shù)列表。它生成T實例,所以我們將類型從List<T>更改為List<? extends T>.棘手的應用是類型參數(shù)T。這是我們第一次看到通配符應用于類型參數(shù)。起初,指定T來擴展Comapable<T>,但可比較的T消費T實例(并生成指示順序關系的整數(shù))。因此,參數(shù)化類型Comparable<T>被有界通配符替換為Comparable<? super T>.可比較性始終是消費者,所以你通常使用Comparable<? super T>來代替Comparable<T>.Comparators也是一樣;因此,你應該通常使用Comparator<? super T>優(yōu)先于Comparator<T>.
??修訂后的max聲明可能是本書中最復雜的方法聲明。增加的復雜性真的給你帶來了什么嗎?再說一次,的確如此。下面是一個清單的簡單例子,它將被原始聲明排除在外,但經修訂的聲明卻允許這樣做:

image.png

??

不能將原始方法聲明應用于此列表的原因是ScheduledFuture并沒有實現(xiàn)Comparable<ScheduledFuture>.相反,它是Delay的子接口,它extends了Comparable<Delayed>。換句話說,一個ScheduledFuture實例不僅僅與其他ScheduledFuture的實例比較;它還要與任何Delayed實例比較,這足以導致原始聲明拒絕它。更普遍地說,通配符需要支持那些不直接實現(xiàn)可比較(或比較器)但擴展了可比較(或比較器)類型的類型。
??還有一個與通配符相關的話題指的討論。類型參數(shù)與通配符之間存在二元性,可以使用其中一種或另一種來聲明許多方法。例如,下面是用于交換列表中兩個索引項的靜態(tài)方法的兩個可能的聲明。第一個參數(shù)使用無界類型參數(shù)( item30),第二個參數(shù)使用無界通配符。

image.png

??這兩種聲明哪個更可取,為什么?在公有API,第二個更好,因為它更簡單。你傳遞了一個列表-任何列表-方法叫喊了元素的索引。沒有類型參數(shù)來擔心。作為一個規(guī)則,如果類型參數(shù)在方法聲明中只出現(xiàn)一次,則用通配符替換它。如果它是一個無界類型參數(shù),用一個無界通配符替換它;如果它是有界類型參數(shù),則用有界通配符替換它。
??swap的第二個聲明有一個問題。簡單的實現(xiàn)不會編譯。

image.png

??試圖編譯它會產生以下不太有用的錯誤消息:


image.png

??

似乎我們不能把一個元素放回我們剛剛從列表中刪除的列表中,這似乎是不對的。問題在于,list的類型是List<?>,不能將除了null以外的任何值放入List<?>.幸運的是,有一種方法可以實現(xiàn)此方法,而不必求助于不安全的類型或原始類型。這個想法是編寫一個私有輔助方法來捕獲通配符類型。這個輔助方法必須是為了捕獲類型的泛型方法。這是它的樣子:

image.png

??swapHelper方法知道list的類型是List<E>.因此,它知道它從這個list中得到的任何值都是E類型的,將E類型的任何值放入list是安全的。這個稍微復雜的交換實現(xiàn)可以清晰地編譯。它允許我們導出基于通配符的聲明,同時利用內部更復雜的泛型方法。交換方法的客戶端不必面對更復雜的swapHelper聲明,但是它們確實從中受益。值得注意的是,helper方法具有我們認為對公共方法來說過于復雜的簽名。
??總之,在你的APIs中使用通配符類型,雖然有點棘手但是使得API更加靈活。如果編寫了一個將被廣泛使用的庫,則應該認為正確使用通配符類型是強制性的。記住基本規(guī)則:producer-extends, consumer-super (PECS)。也記住所有可比較的和比較器都是消費者。
本文寫于2019.6.27,歷時1天

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容