java編譯器能夠檢查所有的方法調用和對應的聲明來決定類型的實參,即類型推測,類型的推測算法推測滿足所有參數的最具體類型,如下例所示:
//泛型方法的聲明 static <T> T pick(T a1, T a2) { return a2; } //調用該方法,根據賦值對象的類型,推測泛型方法的類型參數為Serializable //String和ArrayList<T>都實現接口Serializable,後者是最具體的類型 Serializable s = pick("d", new ArrayList<String>());
類型的推測可以使泛型方法的使用語法和普通的方法一樣,不必指定尖括號內的類型,如上述例子。
對於泛型類的使用,java編譯器也可以進行類型的推測,因此調用泛型類時,可以不用指定尖括號內的類型參數,不過尖括號不可省略,之前的總結已經提到,空的尖括號又叫鑽石(中文怪怪的),如下例所示:
//以下用法沒有指定類型參數,尖括號為空 Map<String, List<String>> myMap = new HashMap<>(); //注意,空的簡括號不能省略,如下代碼編譯器會發出警告 Map<String, List<String>> myMap = new HashMap();
上述代碼中的第二個賦值語句中new HashMap() 實際是用的原始類型。
無論是泛型還是非泛型的類都可以使用泛型的構造器,如方法一樣。
//類定義 class MyClass<X> { <T> MyClass(T t) { // ... } } //以下是實例化以上類的表達式 new MyClass<Integer>("")
以上代碼中的實例化表達式雖然沒有指定構造器的類型參數,但是可以根據傳入的參數推測其類型參數為String。
java7以前的版本能夠推測出構造其的參數類型,而java7以後,使用鑽石的語法也推測泛型類的參數類型。
需要注意的是,類型參數的推測算法只會使用傳入的參數,目的類型或者和明顯的返回類型來推測類型。
java編譯器充分利用了目的類型來推測泛型方法或者類的類型參數,如下例:
//Collections中的一個方法的聲明如下 static <T> List<T> emptyList(); //現在調用該方法 List<String> listOne = Collections.emptyList();
以上中的第二個語句中,listOne變量類型為List<string>,就是目的類型,所以需要方法emptyList的返回類型也必須是List<Stirng>,這樣可以推測泛型方法聲明中的T為String,java7和8都可以實現這樣的推測,當然你可以在調用泛型方法時指明方括號中的類型參數。
值得注意的是java7中方法的參數還不屬於目的類型,而java8則把方法參數加入目的類型,如下例所示:
//如下方法接受的參數為List<String> void processStringList(List<String> stringList) { // process stringList } //Collections中的emptyList方法的簽名如下 static <T> List<T> emptyList(); //java7中,下列調用語句的編譯會報錯,而java8則不存在這樣的問題 processStringList(Collections.emptyList());
在泛型的代碼中問號(?)代表通配符,代表未知的類型,通配符可以用在許多場合,可用作參數,字段,返回值的類型,但是通配符不能用作方法調用,泛型實例的創建和父類型的實參。
利用上限通配符可以放松對變量的限制。
上限通配符的聲明方法如下例所示:
上述聲明的方法,的泛型參數使用了上限通配符,通配符"?"加extends關鍵詞後跟其上限,此處的extends類似於通常意義上的extends和implements,意思是該方法是針對於Number類型的子類型,包括Integer,Float等的列表。
通配符<? extends Foo>匹配所有的Foo的子類型和Foo類型自身。
無限制通配符就是簡單的"?",如List<?>就代表未知類型的列表,以下兩種情況適合使用無限制通配符:
以下例子很好的說明使用Object類中的方法時使用無限制通配符的好處:
//普通的方法聲明 public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); } //使用通配符的泛型作為方法參數,該方法的參數能夠傳入任何類型的列表(List) public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + " "); System.out.println(); }
注意:既然定義了列表List<?>的類型的廣泛性,就要承擔廣泛性的造成的後果,在方法聲明中,只能對List<?>類型的變量插入null,因為你無法預知傳入方法的類型變量,而List<Object>作為參數則可以插入任何類型的對象。
與上限通配符類似,下限通配符指定了類型參數的下限,未知的類型必須是指定類型的父類型,下限通配符的寫法:<? super A>,此處關鍵詞為super。
注意:不能同時指定上限和下限。
之前提到過,泛型之間的關系不僅僅是由他們的類型實參決定的,如不能說List<Number>就是List<Integer>的父類,不過使用通配符可以構成如下關系:
箭頭表示“是其子類型”的關系,如List<Integer>是List<? extends Integer>的子類型,可以這樣理解:List<Integer>是一種List<? extends Integer>。
有時候編譯器會推測通配符的類型,如果一個字段的類型被定義為List<?>,當運算一個表達式的時候,編譯器會從代碼中推測該字段為一個特定的類型,這就叫通配符的捕獲。
import java.util.List; public class WildcardError { void foo(List<?> i) { i.set(0, i.get(0)); } }
上述代碼會編譯出錯,foo方法調用List.set(int,E),編譯器首先將set方法內作為參數的i視為Object類型,無法判斷將要插入的對象類型是否和目標列表類型是否一致,所以編譯不能通過。
此時可以加入一個輔助方法,使其能能夠順利通過編譯:
public class WildcardFixed { void foo(List<?> i) { fooHelper(i); } // 創建輔助方法,調用該方法可以通過類型推測來實現通配符的捕獲 private <T> void fooHelper(List<T> l) { l.set(0, l.get(0)); } }
再來看一下一個例子:
import java.util.List; public class WildcardErrorBad { void swapFirst(List<? extends Number> l1, List<? extends Number> l2) { Number temp = l1.get(0); l1.set(0, l2.get(0)); l2.set(0, temp); } }
上述代碼中的方法功能是將兩個列表的首個元素交換,然而無法判斷兩個傳入的實參的類型參數是否兼容,所以,無法編譯通過,此處代碼本質上就是錯誤的,沒有相應的輔助方法。
泛型的使用有一點讓人疑惑的就是不知道什麼時候該用上限通配符,什麼時候使用下限通配符,一下是幾點原則:
為了說明問題,先列出兩種變量1)In變量:作為代碼中的數據來源,比如復制的方法copy(src,dest)中的src參數就是in變量,;2)out變量,在代碼中用來存儲數據作為他用,如copy(src,dest)中的dest參數就是out變量。變量列出之後,說原則:
上述原則不試用與方法的返回類型,不建議在返回類型中使用通配符,否則將必須處理通配符的問題。