15.10通配符
你已經在第11章中看到了一些使用通配符的示例——在泛型參數表達式中的問號,在第14章中這種示例更多。本節將更深人地探討這個問題。
我們開始入手的示例要展示數組的一種特殊行爲:可以嚮導出類型的數組賦予基類型的數組引用:
//: generics/CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~
main()中的第一行創建了一個Apple數組,井將其賦值給一個Fruit數組引用。這是有意義的,因爲Apple也是一種Fruit,因此Apple數組應該也是一個Fruit數組。
但是,如果實際的數組類型是Apple[],你應該只能在其中放置Apple或Apple的子類型,這在編譯期和運行時都可以工作。但是請注意,編譯器允許你將Fruit放置到這個數組中,這對於編譯器來說是有意義的,因爲它有一個Fruit[]引用——它有什麼理由不允許將Fruit對象或名任何從Fruit繼承出來的對象(例如Orange),放置到這個數組中呢?因此,在編譯期,這是允許的。但是,運行時的數組機制知道它處理的是Apple[],因此會在向數組中放置異構類型時拋出異常。
實際上,向上轉型不合適用在這裏。你真正做的是將一個數組賦值給另一個數組。數組的行爲應該是它可以持有其他對象,這裏只是因爲我們能夠向上轉型而已,所以很明顯,數組對象可以保留有關它們包含的對象類型的規則。就好像數組對它們持有的對象是有意識的,因此在編譯期檢查和運行時檢查之間,你不能濫用它們。
對數組的這種賦值並不是那麼可怕,因爲在運行時可以發現你已經插入了不正確的類型。但是泛型的主要目標之一是將這種錯誤檢測移入到編譯期。因此當我們試圖使用泛型容器來代替數組時,會發生什麼呢?
//: generics/NonCovariantGenerics.java
// {CompileTimeError} (Won't compile)
import java.util.*;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
} ///:~
儘管你在第一次閱讀這段代碼時會認爲:“不能將一個Apple容器賦值給一個Fruit容器”。別忘了,泛型不僅和容器相關。正確的說法是:“不能把一個涉及Apple的泛型賦給一個涉及Fruit的泛型”。如果就像在數組的情況中一樣,編譯器對代碼的瞭解足夠多,可以確定所涉及到的容器,那麼它可能會留下一些餘地。但是它不知透任何有關這方面的信息,因此它拒絕向上轉型。然而實際上這根本不是向上轉型——Apple的List不是Fruit的List。Apple的List將持有Apple和Apple的子類型,而Fruit的List將持有任何類型的Fruit,誠然,這包括Apple在內,但是它不是一個Apple的List,它仍舊是Fruit的List。Apple的List在類型上不等價於Fruit的List,即使Apple是一種Fruit類型。
真正的問題是我們在談論容器的類型。而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型。這是因爲數組在語言中是完全定義的,因此可以內建了編譯期和運行時的檢查,但是在使用泛型時,編譯器和運行時系統都不知道你想用類型做些什麼,以及該採用什麼樣的規則。
但是,有時你想要在兩個類型之間建立某種類型的向上轉型關係,這正是通配符所允許的:
//: generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
} ///:~
flist類型現在是List<? extends Fruit>
,你可以將其讀作“具有任何從Fruit繼承的類型的列表”。但是,這實際上並不意味着這個List將持有任何類型的Fruit。通配符引用的是明確的類型,因此它意味着“某種first引用沒有指定的具體類型”。因此這個被賦值的List必須持有諸如Fruit或Apple這樣的某種指定類型,但是爲了向上轉型爲flist,這個類型是什麼並沒有人關心。
如果唯一的限制是這個List要持有某種具體的Fruit或Fruit的子類型,但是你實際上並不關心它是什麼,那麼你能用這樣的List做什麼呢?如果不知道List持有什麼類型,那麼你怎樣才能安全地向其中添加對象呢?就像在CovariantArrays.java中向上轉型數組一樣,你不能,除非編譯器而不是運行時系統可以阻止這種操作的發生。你很快就會發現這一問題。
你可能會認爲,事情變得有點走極端了,因爲現在你甚至不能向剛剛聲明過將持有Apple對象的List中放置一個Apple對象了。是的,但是編譯器並不知道這一點。List<? extends Fruit>
可以合法地指向一個List<Orange>
。一旦執行這種類型的向上轉型,你就將丟失掉向其中傳遞任何對象的能力,甚至是傳遞Object也不行。
另一方面,如果你調用一個返回Fruit的方法,則是安全的,因爲你知道在這個List中的任何對象至少具有Fruit類型,因此編譯器將允許這麼做。
15.10.1 編譯器有多聰明
現在,你可能會猜想自己被阻止去調用任何接受參數的方法,但是請考慮下面的程序:
//: generics/CompilerIntelligence.java
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
} ///:~
你可以看到,對contains()和indexOf()的調用,這兩個方法都接受Apple對象作爲參數,而這些調用都可以正常執行。這是否意味着編譯器實際上將檢查代碼,以查着是否有某個特定的方法修改了它的對象?
通過查看ArrsyList的文檔,我們可以發現,編譯器並沒有這麼聰明。儘管add()將接受一個具有泛型參數類型的參數,但是contains()和indexOf()將接受Object類型的參數。因此當你指定一個ArrayList<? extends Fruit>
時,add()的參數就變成了“? Extends Fruit”。從這個描述中,編譯器並不能瞭解這裏需要Fruit的哪個具體子類型,因此它不會接受任何類型的Fruit。如果先將Apple向上轉型爲Fruit,也無關緊要——編譯器將直接拒絕對參數列表中涉及通配符的方法(例如add())的調用。
在使用contains()和indexOf()時,參數類型是Object,因此不涉及任何通配符,而編譯器也將允許這個調用。這意味着將由泛型類的設計者來決定哪些調用是“安全的”,並使用Object類型作爲其參數類型。爲了在類型中使用了通配符的情況下禁止這類調用,我們需要在參數列表中使用類型參數。
可以在一個非常簡單的Holder類中看到這一點:
//: generics/Holder.java
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns 'Object'
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
Holder有一個接受T類型對象的set()方法,一個get()方法,以及一個接受Object對象的equals()方法。正如你已經看到的,如果創建了一個Holder<Apple>
,不能將其向上轉型爲Holder<Fruit>
,但是可以將其向上轉型爲Holder<? extends Fruit>
,如果調用get(),它只會返回一個Fruit——這就是在給定“任何擴展自Fruit的對象”這一邊界之後,它所能知道的一切了。
如果能夠了解更多的信息,那麼你可以轉型到某種具體的Fruit類型,而這不會導致任何警告,但是你存在着得到ClassCastException的風險。set()方法不能工作於Apple或Fruit,因爲set()的參數也是”? Extends Fruit”,這意味着它可以是任何事物,而編譯器無法驗證“任何事物”的類型安全性。
但是,equals()方法工作良好,因爲它將接受Object類型而井非T類型的參數。因此,編譯器只關注傳遞進來和要返回的對象類型,它井不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。
15.10.2 逆變
還可以走另外一條路,即使用超類型通配符
。這裏,可以聲明通配符是由某個特定類的任何基類來界定的,方法是指定<? super MyClass>
,甚至或者使用類型參數:<? super T>
(儘管你不能對泛型參數給出一個超類型邊界;即不能聲明<T super MyClass>
)。這使得你可以安全地傳遞一個類型對象到泛型類型中。因此,有了超類型通配符,就可以向Collection寫入了:
//: generics/SuperTypeWildcards.java
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
} ///:~
參數Apple是Apple的某種基類型的List,這樣你就知道向其中添加Apple或Apple的子類型是安全的。但是,既然Apple是下界,那麼你可以知道向這樣的List中添加Fruit是不安全的,因爲這將使這個List敞開口子,從而可以向其中添加非Apple類型的對象,而這是違反靜態類型安全的。
因此你可能會根據如何能夠向一個泛型類型“寫人”(傳遞給一個方法),以及如何能夠從一個泛型類型中“讀取”(從一個方法中返回),來着手思考子類型和超類型邊界。
超類型邊界放鬆了在可以向方法傳遞的參數上所作的限制:
//: generics/GenericWriting.java
import java.util.*;
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static void f1() {
writeExact(apples, new Apple());
// writeExact(fruit, new Apple()); // Error:
// Incompatible types: found Fruit, required Apple
}
static <T> void
writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) { f1(); f2(); }
} ///:~
writeExact()方法使用了一個確切參數類型(無通配符)。在f1()中,可以看到這工作良好——只要你只向List<Apple>
中放置Apple。但是,writeExact()不允許將Apple放置到List<Fruit>
中,即使知道這應該是可以的。
在writeWithWildcard()中,其參數現在是List<? super T>
,因此這個List將持有從T導出的某種具體類型,這樣就可以安全地將一個T類型的對象或者從T導出的任何對象作爲參數傳遞給List的方法。在f2()中可以看到這一點,在這個方法中我們仍舊可以像前面那樣,將Apple放置到List<Apple>
中,但是現在我們還可以如你所期望的那樣,將Apple放置到List<Fruit>
中。
我們可以執行下面這個相同的類型分析。作爲對協變和通配符的一個複習:
//: generics/GenericReading.java
import java.util.*;
public class GenericReading {
static <T> T readExact(List<T> list) {
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
// A static method adapts to each call:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// If, however, you have a class, then its type is
// established when the class is instantiated:
static class Reader<T> {
T readExact(List<T> list) { return list.get(0); }
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Fruit a = fruitReader.readExact(apples); // Error:
// readExact(List<Fruit>) cannot be
// applied to (List<Apple>).
}
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader =
new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1(); f2(); f3();
}
} ///:~
與前面一樣,第一個方法readExact()使用了精確的類型。因此如果使用這個沒有任何通配符的精確類型,就可以向List中寫人和讀取這個精確類型。另外,對於返回值,靜態的泛型方法readExact()可以有效地“適應”每個方法調用,並能夠從List<Apple>
中返回一個Apple,從List<Fruit>
中返回一個Fruit,就像在f1()中看到的那樣。因此,如果可以擺脫靜態泛型方法,那麼當只是讀取時,就不需要協變類型了。
但是,如果有一個泛型類,那麼當你創建這個類的實例時,要爲這個類確定參數。就像在f2()中看到的,fruitReader實例可以從List<Fruit>
中讀取一個Fruit,因爲這就是它的確切類型。但是List<Apple>
還應該產生Fruit對象,而fruitReader不允許這麼做。
爲了修正這個問題,CovariantReader.readCovcariant()方法將接受List<? extends T>
,因此,從這個列表中讀取一個T是安全的(你知道在這個列表中的所有對象至少是一個T,並且可能是從T導出的某種對象)。在f3()中,你可以看到現在可以從List<Apple>
中讀取Fruit了。
15.10.3 無界通配符
無界通配符<?>
看起來意味着“任何事物”,因此使用無界通配符好像等價於使用原生類型。
事實上,編譯器初看起來是支持這種判斷的:
//: generics/UnboundedWildcards1.java
import java.util.*;
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
// list3 = list; // Warning: unchecked conversion
// Found: List, Required: List<? extends Object>
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
// assign3(new ArrayList()); // Warning:
// Unchecked conversion. Found: ArrayList
// Required: List<? extends Object>
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
// Both forms are acceptable as List<?>:
List<?> wildList = new ArrayList();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
} ///:~
有很多情況都和你在這裏看到的情況類似,即編譯器很少關心使用的是原生類型還是<?>
。在這些情況中,<?>
可以被認爲是一種裝飾,但是它仍舊是很有價值的,因爲,實際上,它是在聲明:“我是想用Java的泛型來編寫這段代碼,我在這裏並不是要用原生類型,但是在當前這種情況下,泛型參數可以持有任何類型。”
第二個示例展示了無界通配符的一個重要應用。當你在處理多個泛型參數時,有時允許一個參數可以是任何類型,同時爲其他參數確定某種特定類型的這種能力會顯得很重要:
//: generics/UnboundedWildcards2.java
import java.util.*;
public class UnboundedWildcards2 {
static Map map1;
static Map<?,?> map2;
static Map<String,?> map3;
static void assign1(Map map) { map1 = map; }
static void assign2(Map<?,?> map) { map2 = map; }
static void assign3(Map<String,?> map) { map3 = map; }
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
// assign3(new HashMap()); // Warning:
// Unchecked conversion. Found: HashMap
// Required: Map<String,?>
assign1(new HashMap<String,Integer>());
assign2(new HashMap<String,Integer>());
assign3(new HashMap<String,Integer>());
}
} ///:~
但是,當你擁有的全都是無界通配符時,就像在Map<?,?>
中看到的那樣,編譯器看起來就無法將其與原生Map區分開了。另外,UnboundedWildcards.java展示了編譯器處理List<?>
和List<? extends Object>
時是不同的。
令人困惑的是,編譯器並非總是關注像List和List<?>
之間的這種差異,因此它們看起來就像是相同的事物。因爲,事實上,由幹泛型參數將擦除到它的第一個邊界,因此List<?>
看起來等價於List<Object>
,而List實際上也是List<Object>
——除非這些語句都不爲真。List實際上表示“持有任何Object類型的原生List”,而List<?>
表示“具有某種特定類型
的非原生List,只是我們不知道那種類型是什麼。”
編譯器何時纔會關注原生類型和涉及無界通配符的類型之間的差異呢?下面的示例使用了前面定義的Holder<T>
類,它包含接受Holder作爲參數的各種方法,但是它們具有不同的形式作爲原生類型,具有具體的類型參數以及具有無界通配符參數:
//: generics/Wildcards.java
// Exploring the meaning of wildcards.
public class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
// holder.set(arg); // Warning:
// Unchecked call to set(T) as a
// member of the raw type Holder
// holder.set(new Wildcards()); // Same warning
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
// Similar to rawArgs(), but errors instead of warnings:
static void unboundedArg(Holder<?> holder, Object arg) {
// holder.set(arg); // Error:
// set(capture of ?) in Holder<capture of ?>
// cannot be applied to (Object)
// holder.set(new Wildcards()); // Same error
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder) {
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
T t = holder.get();
return t;
}
static <T>
T wildSubtype(Holder<? extends T> holder, T arg) {
// holder.set(arg); // Error:
// set(capture of ? extends T) in
// Holder<capture of ? extends T>
// cannot be applied to (T)
T t = holder.get();
return t;
}
static <T>
void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
// T t = holder.get(); // Error:
// Incompatible types: found Object, required T
// OK, but type information has been lost:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
// Or:
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
// Object r1 = exact1(raw); // Warnings:
// Unchecked conversion from Holder to Holder<T>
// Unchecked method invocation: exact1(Holder<T>)
// is applied to (Holder)
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Must return Object
Long r4 = exact1(bounded);
// Long r5 = exact2(raw, lng); // Warnings:
// Unchecked conversion from Holder to Holder<Long>
// Unchecked method invocation: exact2(Holder<T>,T)
// is applied to (Holder,Long)
Long r6 = exact2(qualified, lng);
// Long r7 = exact2(unbounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied to
// (Holder<capture of ?>,Long)
// Long r8 = exact2(bounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied
// to (Holder<capture of ? extends Long>,Long)
// Long r9 = wildSubtype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? extends Long>
// Unchecked method invocation:
// wildSubtype(Holder<? extends T>,T) is
// applied to (Holder,Long)
Long r10 = wildSubtype(qualified, lng);
// OK, but can only return Object:
Object r11 = wildSubtype(unbounded, lng);
Long r12 = wildSubtype(bounded, lng);
// wildSupertype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? super Long>
// Unchecked method invocation:
// wildSupertype(Holder<? super T>,T)
// is applied to (Holder,Long)
wildSupertype(qualified, lng);
// wildSupertype(unbounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ?>,Long)
// wildSupertype(bounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ? extends Long>,Long)
}
} ///:~
在rawArgs()中,編譯器知道Holder是一個泛型類型,因此即使它在這裏被表示成一個原生類型,編譯器仍舊知道向set()傳遞一個Object是不安全的。由於它是原生類型,你可以將任何類型的對象傳遞給set(),而這個對象將被向上轉型爲Object。因此,無論何時,只要使用了原生類型,都會放棄編譯期檢查。對get()的調用說明了相同的問題:沒有任何T類型的對象,因此結果只能是一個Object。
人們很自然地會開始考慮原生Holder與Holder<?>
是大致相同的事物。但是unboundedArg()強調它們是不同的——它揭示了相同的問題,但是它將這些問題作爲錯誤而不是警告報告,因爲原生Holder將持有任何類型的組合,而Holder<?>
將持有具有某種具體類型的同構集合,因此不能只是向其中傳遞Object。
在exact1()和exact2()中,你可以看到使用了確切的泛型參數沒有任何通配符。你將看到,exact2()與exact1()具有不同的限制,因爲它有額外的參數。
在wildSubtype()中,在Holder類型上的限制被放鬆爲包括持有任何擴展自T的對象的Holder。這還是意味着如果T是Fruit,那麼Holder可以是Holder<Apple>
,這是合法的。爲了防止將Orange放置到Holder<Apple>
中,對set()的調用(或者對任何接受這個類型參數爲參數的方法的調用)都是不允許的。但是,你仍舊知道任何來自Holder<? extends Fruit>
的對象至少是Fruit,因此get()(或者任何將產生具有這個類型參數的返回值的方法)都是允許的。
wildSupertype()展示了超類型通配符,這個方法展示了與wildSubtype()相反的行爲:holder可以是持有任何T的基類型的容器。因此,set()可以接受T,因爲任何可以工作於基類的對象都可以多態地作用於導出類(這裏就是T)。但是,嘗試着調用get()是沒有用的,因爲由holde持有的類型可以是任何超類型,因此唯一安全的類型就是Object。
這個示例還展示了對於在unbounded()中使用無界通配符能夠做什麼不能做什麼所做出的限制。對於遷移兼容性,rawArgs()將接受所有Holder的不同變體,而不會產生警告。unbounded-Args()方法也可以接受相同的所有類型,儘管如前所述,它在方法體內部處理這些類型的方式並不相同。
如果向接受“確切”泛型類型(沒有通配符)的方法傳遞一個原生Holder引用,就會得到一個警告,因爲確切的參數期望得到在原生類型中井不存在的信息。如果向exact1()傳遞一個無界引用,就不會有任何可以確定返回類型的類型信息。
可以看到,exact2()具有最多的限制,因爲它希望精確地得到一個Holder<T>
,以及一個具有類型T的參數,正由於此,它將產生錯誤或警告,除非提供確切的參數。有時,這樣做很好,但是如果它過於受限,那麼就可以使用通配符,這取決於是否想要從泛型參數中返回類型確定的返回值(就像在wildSubtype()中看到的那樣),或者是否想要向泛型參數傳遞類型確定的參數(就像在wildSupertype()中看到的那樣)。
因此,使用確切類型來替代通配符類型的好處是,可以用泛型參數來做更多的事,但是使用通配符使得你必須接受範圍更寬的參數化類型作爲參數。因此,必須逐個情況地權衡利弊,找到更適合你的需求的方法。
15.10.4 捕獲轉換
有一種情況特別需要使用<?>
而不是原生類型。如果向一個使用<?>
的方法傳遞原生類型,那麼對編譯器來說,可能會推斷出實際的類型參數,使得這個方法可以迴轉並調用另一個使用這個確切類型的方法。下面的示例演示了這種技術,它被稱爲捕獲轉換,因爲未指定的通配符類型被捕獲,並被轉換爲確切類型。這裏,有關警告的註釋只有在@SuppressWarnings註解被移除之後才能起作用:
//: generics/CaptureConversion.java
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder); // Call with captured type
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
// f1(raw); // Produces warnings
f2(raw); // No warnings
Holder rawBasic = new Holder();
rawBasic.set(new Object()); // Warning
f2(rawBasic); // No warnings
// Upcast to Holder<?>, still figures it out:
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
} /* Output:
Integer
Object
Double
*///:~
f1()中的類型參數都是確切的,沒有通配符或邊界。在f2()中,Holder()參數是一個無界通配符,因此它看起來是未知的。但是,在f2()中,f1()被調用,而f1()需要一個已知參數。這裏所發生的是:參數類型在調用f2()的過程中被捕獲,因此它可以在對f1()的調用中被使用。
你可能想知道,這項技術是否可以用於寫入,但是這要求要在傳遞Holder<?>
時同時傳遞一個具體類型。捕獲轉換只有在這樣的情況下可以工作:即在方法內部,你需要使用確切的類型。注意,不能從f2()中返回T,因爲T對於f2()來說是未知的。捕獲轉換十分有趣,但是非常受限。