1、通配符
可以嚮導出類型的數組賦予基類型的數組引用
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();
fruit[1] = new Jonathan();
try{
// java.lang.ArrayStoreException
fruit[0] = new Fruit();
}catch (Exception e){
System.out.println(e);
}
try{
// java.lang.ArrayStoreException
fruit[0] = new Orange();
}catch (Exception e){
System.out.println(e);
}
}
}
創建了一個Apple數組,並將其賦值給一個Fruit數組引用,有意義的,因爲Apple也是一種Fruit,因此Apple數組應該也是一個Fruit數組。
但是,實際的數組類型是Apple[],應該只能放置Apple或apple的子類型,這在編譯器和運行時都可以運行。編譯器運行你將Fruit放在這個數組中,是因爲它有一個Fruit[]引用——因此他運行將Fruit對象或任何從Fruit繼承的對象(Orange)放在這個數組中,所以,編譯期是運行的,但是,運行時數組機制知道它是Apple[],因此會在向數組中放置異構類型 拋出異常。
實際中向上轉型不合適用在這,我們真正做的是將一個數組賦值給另一個數組。數組的持有其他對象行爲是因爲向上轉型而已,數組本身對自己持有的對象有檢測,因此在編譯期和運行時檢查,我們要小心。
因爲泛型的主要目標之一是將運行時錯誤移入到編譯器,因此我們接下來嘗試用泛型容器來代替數組。
// Compile Error
ArrayList<Fruit> apples = new ArrayList<Apple>();
泛型和容器相關正確的說法爲:不能把一個涉及Apple的泛型賦給一個設計Fruit的泛型。如果像在數組的情況中一樣,編譯器對代碼的瞭解足夠多,可以確定所涉及到的容器,那麼它可能運行編譯時通過,但是它不知道任何有關這方面的信息,因此它拒絕向上轉型。實際上着也不是向上轉型——Apple的List不是Fruit的List。Apple的List將持有Apple和Apple的子類型,而Fruit的List將持有任何類型的Fruit,誠然包括Apple,但是他不是一個Apple的List,它仍舊是Fruit的List。Apple的List在類型上不等價與Fruit的List,即使Apple是一種Fruit類型。
真正的問題是容器的類型,而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型。這是因爲數組在語言中是完全定義的,隱藏可以內建編譯期和運行時的檢查,但是在使用泛型時,編譯器和運行時系統都不知道你想用類型做什麼,以及應該採用什麼規則。
有時你想要在兩個類型之間建立某種類型的向上轉型關係,可以通過通配符。
public static void main(String[] args) {
ArrayList<? extends Fruit> flist = new ArrayList<>();
// Compile Error
// flist.add(new Apple());
// Compile Error
// flist.add(new Fruit());
// Compile Error
// flist.add(new Orange());
flist.add(null);
Fruit fruit = flist.get(0);
}
flist類型現在是List<? extends Fruit>,讀作“具有任何從Fruit繼承的類型的列表”,但是,這實際上並不意味着這個List能持有任何類型的Fruit,通配符引用的是明確的類型,因此它意味着“某種flist引用沒有指定的具體類型”。因此這個被賦值的List必須持有諸如Fruit或Apple這樣的類型,但是爲了向上轉型成flist,這個類型是什麼沒人關係。
因爲我們不知道List持有什麼類型,我們就不能安全向其中添加對象,因此編譯期就會阻止我們添加對象。
另一方面,如果我們調用返回Fruit的方法,則是安全的,因爲這個List中的任何對象至少具有Fruit類型,因此編譯器允許我們get(0).
2、編譯器
你可能以爲自己被阻止去調用任何接受參數的方法,其實不然
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple apple = (Apple) flist.get(0);
System.out.println(flist.contains(new Apple()));
System.out.println( flist.indexOf(new Apple()));
}
}
contains和indexOf都可以接受Apple對象,並且可以正常執行。是否意味着編譯器實際將檢查代碼,來查看是否有特定的方法修改了他的對象?
通過ArrayList的文檔,我們可以發現add接受一個泛型參數類型的參數,但是contains和indexOf將接受Object類型的參數,因此,當指定一個 ArrayList<? extends Fruit>時,add的參數就變成了? extends Fruit.因此,編譯器並不能瞭解這裏需要Fruit的哪個具體子類型,因此它不會接受任何類型的Fruit,如果將Apple向上轉型爲Fruit,編譯器將直接拒絕對參數列表中涉及通配符的方法(add)的調用,也就是不能直接add(new Fruit())。
在contains和indexOf,參數類型爲Object,因此不涉及任何通配符,而編譯器也將允許調用。意味着泛型類的設計者決定哪些調用時安全的,並用Object類作爲參數類型。爲了在類型中使用了通配符的情況下禁止這類調用,我們需要在參數列表中使用類型參數
public class Holder<T> {
private T value;
public Holder() {
}
public Holder(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value.equals(o);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
public static void main(String[] args) {
Holder<Apple> appleHolder = new Holder<>(new Apple());
Apple value = appleHolder.getValue();
appleHolder.setValue(value);
// cannot upcast
// Holder<Fruit> fruit = appleHolder;
Holder<? extends Fruit> fruit = appleHolder;
Fruit f = fruit.getValue();
value = (Apple)fruit.getValue();
try{
Orange c = (Orange)fruit.getValue();
} catch (Exception e){
System.out.println(e);
}
// cannot call set
fruit.setValue(new Apple());
// cannot call set
fruit.setValue(new Fruit());
System.out.println(fruit.equals(value));
}
}
如果創建了一個Holder<Apple>不能向上轉型爲Holder<Fruit>,但是可以向上轉型爲Holder<? extends Fruit>如果調用getValue,只會返回一個Fruit——在給定“任何擴展自Fruit的對象”這一邊界後,它所能知道的一切了。如果你能夠了解更多的信息,比如強制轉換成Apple【某種具體的Fruit類型】,這不會導致任何警告,但是存在着ClassCastException【轉成orange】。set方法不能作用於apple或Fruit,是因爲setValue的參數也是“? extends Fruit”,這意味着它可以是任何事物,而編譯器無法驗證“任何事物”的類型安全性。
但是equals由於它接受的是Object而非T類型,因此,編譯器只關注傳遞進來和要返回的對象類型,它並不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。
3、逆變
超類型通配符,可以聲明通配符是由某個特定類的任何基類來界定的,方法用<? super MyClass> 類型參數<? super T>儘管你不能對泛型參數給出一個超類型邊界,即不能聲明<T super MyClass>。super使得我們可以安全地傳遞一個類型對象到泛型類型中。
static void writeTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
// Error
// apples.add(new Fruit());
}
參數Apple是Apple的某種基類型的List,這樣我們可以向其中安全添加Apple或Apple的子類型。既然Apple是下界,那麼我們添加Fruit顯然是不安全的,會導致這個List擴大接納範圍,從而可以向其中添加非Apple類型的對象,這是違反靜態類型安全的。
我們可以根據能夠像一個泛型類型“寫入”(傳遞給一個方法),以及從一個泛型類型中讀取(從一個方法返回),來思考子類型和超類型邊界。
超類型邊界放鬆了可以向方法傳遞的參數上所做的限制。
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());
// 未知版本會出現不自動向上轉型而 出現Error
writeExact(fruit,new Fruit());
}
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沒有使用通配符可能導致不允許將Apple放到List<Fruit>中,即使知道這應該可以的。
writeWithWildcard中,其參數爲List<? super T>因此這個List將持有從T導出的某種具體類型。這樣就可以安全地將一個T類型的對象或者從T導出的任何對象作爲參數傳遞給List的方法。
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> fruits = Arrays.asList(new Fruit());
static void f1(){
Apple apple = readExact(apples);
Fruit fruit = readExact(fruits);
fruit = readExact(apples);
}
/**
* 如果是一個類,那麼他的類型在這個類初始化完成後就被建立關係
*/
static class Reader<T>{
T readExact(List<T> list){return list.get(0);}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<>();
Fruit fruit = fruitReader.readExact(fruits);
// compile error readExact(List<Fruit>) cannot be applied to List<Apple>
// Fruit a = fruitReader.readExact(apples);
}
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
Fruit fruit = fruitCovariantReader.readCovariant(fruits);
Fruit fruit2 = fruitCovariantReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();f2();f3();
}
}
readExact使用了精確類型。因此如果使用這個沒有任何通配符的精確類型,就可以向List寫入和讀取這個精確類型。對於返回值,靜態的泛型方法可以有效地“適應”每個方法調用,並從List<Apple>返回Apple,List<Fruit>返回一個Fruit。因此,如果可以擺脫靜態泛型方法,那麼當只是讀取時,就不需要協變類型了。
但是,當我們使用泛型類,並創建這個類的實例時,要爲這個類確定參數,從fruitReader中List<Fruit>可以讀取一個Fruit,因爲是它的確切類型,但是List<Apple>還應該產生Fruit對象,而fruitReader不允許這麼做。
爲了解決上述問題,CovariantReader方法將接受List<? extends T>。因此從這個列表中讀取一個T是安全的(你知道這個列表中的所有對象至少是一個T,並且可能是從T導出的對象)
Thinking In Java Part12(通配符、超類型通配符)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.