?
Java 理論與實踐: 使用通配符簡化泛型使用
理解通配符捕獲
英文原文級別: 高級
Brian Goetz ( brian.goetz@sun.com ), 高級工程師, Sun Microsystems
2008 年 5 月 26 日
通配符是 Java? 語言中最復雜的泛型之一,特別是圍繞 捕獲通配符 的處理和令人困惑的錯誤消息。在這一期的 Java 理論與實踐 中,資深 Java 開發人員 Brian Goetz 解釋了一些由 javac 生成的怪異錯誤消息并提供了一些簡化泛型使用的技巧和解決方法。自從泛型被添加到 JDK 5 語言以來,它一直都是一個頗具爭議的話題。一部分人認為泛型簡化了編程,擴展了類型系統從而使編譯器能夠檢驗類型安全;另外一些人認為泛型添加了很多不必要的復雜性。對于泛型我們都經歷過一些痛苦的回憶,但毫無疑問通配符是最棘手的部分。
泛型是一種表示類或方法行為對于未知類型的類型約束的方法,比如 “不管這個方法的參數
x
和y
是哪種類型,它們必須是相同的類型”,“必須為這些方法提供同一類型的參數” 或者 “foo()
的返回值和bar()
的參數是同一類型的”。通配符 — 使用一個奇怪的問號表示類型參數 — 是一種表示未知類型的類型約束的方法。通配符并不包含在最初的泛型設計中(起源于 Generic Java(GJ)項目),從形成 JSR 14 到發布其最終版本之間的五年多時間內完成設計過程并被添加到了泛型中。
通配符在類型系統中具有重要的意義,它們為一個泛型類所指定的類型集合提供了一個有用的類型范圍。對泛型類
ArrayList
而言,對于任意(引用)類型T
,ArrayList<?>
類型是ArrayList<T>
的超類型(類似原始類型ArrayList
和根類型Object
,但是這些超類型在執行類型推斷方面不是很有用)。通配符類型
List<?>
與原始類型List
和具體類型List<Object>
都不相同。如果說變量x
具有List<?>
類型,這表示存在一些T
類型,其中x
是List<T>
類型,x
具有相同的結構,盡管我們不知道其元素的具體類型。這并不表示它可以具有任意內容,而是指我們并不了解內容的類型限制是什么 — 但我們知道 存在 某種限制。另一方面,原始類型List
是異構的,我們不能對其元素有任何類型限制,具體類型List<Object>
表示我們明確地知道它能包含任何對象(當然,泛型的類型系統沒有 “列表內容” 的概念,但可以從List
之類的集合類型輕松地理解泛型)。通配符在類型系統中的作用部分來自其不會發生協變(covariant)這一特性。數組是協變的,因為
Integer
是Number
的子類型,數組類型Integer[]
是Number[]
的子類型,因此在任何需要Number[]
值的地方都可以提供一個Integer[]
值。另一方面,泛型不是協變的,List<Integer>
不是List<Number>
的子類型,試圖在要求List<Number>
的位置提供List<Integer>
是一個類型錯誤。這不算很嚴重的問題 — 也不是所有人都認為的錯誤 — 但泛型和數組的不同行為的確引起了許多混亂。清單 1 展示了一個簡單的容器(container)類型
Box
,它支持put
和get
操作。Box
由類型參數T
參數化,該參數表示 Box 內容的類型,Box<String>
只能包含String
類型的元素。public interface Box<T> { public T get(); public void put(T element); }通配符的一個好處是允許編寫可以操作泛型類型變量的代碼,并且不需要了解其具體類型。例如,假設有一個
Box<?>
類型的變量,比如清單 2unbox()
方法中的box
參數。unbox()
如何處理已傳遞的 box?public void unbox(Box<?> box) { System.out.println(box.get()); }事實證明 Unbox 方法能做許多工作:它能調用
get()
方法,并且能調用任何從Object
繼承而來的方法(比如hashCode()
)。它惟一不能做的事是調用put()
方法,這是因為在不知道該Box
實例的類型參數T
的情況下它不能檢驗這個操作的安全性。由于box
是一個Box<?>
而不是一個原始的Box
,編譯器知道存在一些T
充當box
的類型參數,但由于不知道T
具體是什么,您不能調用put()
因為不能檢驗這么做不會違反Box
的類型安全限制(實際上,您可以在一個特殊的情況下調用put()
:當您傳遞null
字母時。我們可能不知道T
類型代表什么,但我們知道null
字母對任何引用類型而言是一個空值)。關于
box.get()
的返回類型,unbox()
了解哪些內容呢?它知道box.get()
是某些未知T
的T
,因此它可以推斷出get()
的返回類型是T
的擦除(erasure),對于一個無上限的通配符就是Object
。因此清單 2 中的表達式box.get()
具有Object
類型。清單 3 展示了一些似乎 應該 可以工作的代碼,但實際上不能。它包含一個泛型
Box
、提取它的值并試圖將值放回同一個Box
。public void rebox(Box<?> box) { box.put(box.get()); } Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied to (java.lang.Object) box.put(box.get()); ^ 1 error這個代碼看起來應該可以工作,因為取出值的類型符合放回值的類型,然而,編譯器生成(令人困惑的)關于 “capture#337 of ?” 與
Object
不兼容的錯誤消息。“capture#337 of ?” 表示什么?當編譯器遇到一個在其類型中帶有通配符的變量,比如
rebox()
的box
參數,它認識到必然有一些T
,對這些T
而言box
是Box<T>
。它不知道T
代表什么類型,但它可以為該類型創建一個占位符來指代T
的類型。占位符被稱為這個特殊通配符的 捕獲(capture) 。這種情況下,編譯器將名稱 “capture#337 of ?” 以box
類型分配給通配符。每個變量聲明中每出現一個通配符都將獲得一個不同的捕獲,因此在泛型聲明foo(Pair<?,?> x, Pair<?,?> y)
中,編譯器將給每四個通配符的捕獲分配一個不同的名稱,因為任意未知的類型參數之間沒有關系。錯誤消息告訴我們不能調用
put()
,因為它不能檢驗put()
的實參類型與其形參類型是否兼容 — 因為形參的類型是未知的。在這種情況下,由于?
實際表示 “?extends Object” ,編譯器已經推斷出box.get()
的類型是Object
,而不是 “capture#337 of ?”。它不能靜態地檢驗對由占位符 “capture#337 of ?” 所識別的類型而言Object
是否是一個可接受的值。雖然編譯器似乎丟棄了一些有用的信息,我們可以使用一個技巧來使編譯器重構這些信息,即對未知的通配符類型命名。清單 4 展示了
rebox()
的實現和一個實現這種技巧的泛型助手方法(helper):public void rebox(Box<?> box) { reboxHelper(box); } private<V> void reboxHelper(Box<V> box) { box.put(box.get()); }助手方法
reboxHelper()
是一個 泛型方法 ,泛型方法引入了額外的類型參數(位于返回類型之前的尖括號中),這些參數用于表示參數和/或方法的返回值之間的類型約束。然而就reboxHelper()
來說,泛型方法并不使用類型參數指定類型約束,它允許編譯器(通過類型接口)對 box 類型的類型參數命名。捕獲助手技巧允許我們在處理通配符時繞開編譯器的限制。當
rebox()
調用reboxHelper()
時,它知道這么做是安全的,因為它自身的box
參數對一些未知的T
而言一定是Box<T>
。因為類型參數V
被引入到方法簽名中并且沒有綁定到其他任何類型參數,它也可以表示任何未知類型,因此,某些未知T
的Box<T>
也可能是某些未知V
的Box<V>
(這和 lambda 積分中的 α 減法原則相似,允許重命名邊界變量)。現在reboxHelper()
中的表達式box.get()
不再具有Object
類型,它具有V
類型 — 并允許將V
傳遞給Box<V>.put()
。我們本來可以將
rebox()
聲明為一個泛型方法,類似reboxHelper()
,但這被認為是一種糟糕的 API 設計樣式。此處的主要設計原則是 “如果以后絕不會按名稱引用,則不要進行命名”。就泛型方法來說,如果一個類型參數在方法簽名中只出現一次,它很有可能是一個通配符而不是一個命名的類型參數。一般來說,帶有通配符的 API 比帶有泛型方法的 API 更簡單,在更復雜的方法聲明中類型名稱的增多會降低聲明的可讀性。因為在需要時始終可以通過專有的捕獲助手恢復名稱,這個方法讓您能夠保持 API 整潔,同時不會刪除有用的信息。捕獲助手技巧涉及多個因素:類型推斷和捕獲轉換。Java 編譯器在很多情況下都不能執行類型推斷,但是可以為泛型方法推斷類型參數(其他語言更加依賴類型推斷,將來我們可以看到 Java 語言中會添加更多的類型推斷特性)。如果愿意,您可以指定類型參數的值,但只有當您能夠命名該類型時才可以這樣做 — 并且不能夠表示捕獲類型。因此要使用這種技巧,要求編譯器能夠為您推斷類型。捕獲轉換允許編譯器為已捕獲的通配符產生一個占位符類型名,以便對它進行類型推斷。
當解析一個泛型方法的調用時,編譯器將設法推斷類型參數它能達到的最具體類型。 例如,對于下面這個泛型方法:
public static<T> T identity(T arg) { return arg };和它的調用:
Integer i = 3; System.out.println(identity(i));編譯器能夠推斷
T
是Integer
、Number
、 Serializable 或Object
,但它選擇Integer
作為滿足約束的最具體類型。當構造泛型實例時,可以使用類型推斷減少冗余。例如,使用
Box
類創建Box<String>
要求您指定兩次類型參數String
:Box<String> box = new BoxImpl<String>();即使可以使用 IDE 執行一些工作,也不要違背 DRY(Don't Repeat Yourself)原則。然而,如果實現類
BoxImpl
提供一個類似清單 5 的泛型工廠方法(這始終是個好主意),則可以減少客戶機代碼的冗余:public class BoxImpl<T> implements Box<T> { public static<V> Box<V> make() { return new BoxImpl<V>(); } ... }如果使用
BoxImpl.make()
工廠實例化一個Box
,您只需要指定一次類型參數:Box<String> myBox = BoxImpl.make();泛型
make()
方法為一些類型V
返回一個Box<V>
,返回值被用于需要Box<String>
的上下文中。編譯器確定String
是V
能接受的滿足類型約束的最具體類型,因此此處將V
推斷為String
。您還可以手動地指定V
的值:Box<String> myBox = BoxImpl.<String>make();除了減少一些鍵盤操作以外,此處演示的工廠方法技巧還提供了優于構造函數的其他優勢:您能夠為它們提高更具描述性的名稱,它們能夠返回命名返回類型的子類型,它們不需要為每次調用創建新的實例,從而能夠共享不可變的實例(參見 參考資料 中的 Effective Java, Item #1,了解有關靜態工廠的更多優點)。
通配符無疑非常復雜:由 Java 編譯器產生的一些令人困惑的錯誤消息都與通配符有關,Java 語言規范中最復雜的部分也與通配符有關。然而如果使用適當,通配符可以提供強大的功能。此處列舉的兩個技巧 — 捕獲助手技巧和泛型工廠技巧 — 都利用了泛型方法和類型推斷,如果使用恰當,它們能顯著降低復雜性。
學習
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文 。
- Java 理論與實踐 (Brian Goetz,developerWorks):參閱該系列的所有文章。
- “ 了解泛型 ”(Brian Goetz,developerWorks,2005 年 1 月):了解如何在學習使用泛型時識別和避免一些陷阱。
- JDK 5.0 中的泛型介紹 (Brian Goetz,developerWorks,2004 年 12 月):developerWorks 投稿人和 Java 編程專家 Brian Goetz 解釋了將泛型添加到 Java 語言的動機、語法細節和泛型類型的語義,并介紹了如何在自己的類中使用泛型。
- JSR 14 :將泛型添加到 Java 編程語言中。早期的規范來源于 GJ 。 通配符 是后來添加的。
- Java Generics and Collections :提供了一個全面的泛型處理。
- Effective Java : Item 1 進一步探討了靜態工廠方法的優點。
- Generics FAQ : Angelika Langer 創建了關于泛型的完整 FAQ。
- Java Concurrency in Practice :使用 Java 代碼開發并發程序的 how-to 手冊,包括構造和組成線程安全的類和程序、避免風險、管理性能和測試并發應用程序。
- 技術書店 :瀏覽有關各種技術主題的書籍。
- Java 技術專區 :數百篇關于 Java 編程各個方面的文章。
討論
![]()
![]()
Brian Goetz 作為一名專業軟件開發人員已經 20 年了。他是 Sun Microsystems 的高級工程師,并且效力于多個 JCP 專家組。Brian 的著作 Java Concurrency In Practice 在 2006 年 5 月由 Addison-Wesley 出版。請參閱 Brian 在流行的業界出版物上 已發表和即將發表的文章 。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
