目錄
邊界
通配符
編譯器的能力范疇
逆變性
無界通配符
捕獲轉換
本筆記參考自: 《On Java 中文版》
邊界
??????? 在泛型中,邊界的作用是:在參數類型上增加限制。這么做可以強制執行應用泛型的類型規則,但還有一個更重要的潛在效果,我們可以調用邊界類型上的方法了。
??? 若一個泛型參數沒有邊界,那么我們只能調用其中的Object方法。
??????? 為了應用邊界的限制,Java復用了extend關鍵字:
【例子:使用extend規定泛型邊界】
interface HasColor {java.awt.Color getColor();
}class WithColor<T extends HasColor> {T item;WithColor(T item) {this.item = item;}T getItem() {return item;}// 可以調用位于邊界上的方法:java.awt.Color color() {return item.getColor();}
}class Coord {public int x, y, z;
}// 在規定邊界時,需要將類排(Coord)在前面,接口(HasColor)排在后面\
// 因此這種寫法會失敗:
// class WithColorCoord<T extends HasColor & Coord> {}// 這樣才能正確定義多重邊界:
class WithColorCoord<T extends Coord & HasColor> {T item;WithColorCoord(T item) {this.item = item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}interface Weight {int weight();
}// 與繼承一樣,只能繼承一個具體類,但可以實現多個接口:
class Solid<T extends Coord & HasColor & Weight> {T item;Solid(T item) {this.item = item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}int weight() {return item.weight();}
}class Bounded extends Coord implements HasColor, Weight {@Overridepublic java.awt.Color getColor() {return null;}@Overridepublic int weight() {return 0;}
}public class BasicBounds {public static void main(String[] args) {Solid<Bounded> solid =new Solid<>(new Bounded());solid.color();solid.getY();solid.weight();}
}
??????? 可以通過繼承去除上例中的一些冗余代碼。繼承也可以增加邊界的限制:
【例子:使用繼承簡化代碼】
class HoldItem<T> {T item;HoldItem(T item) {this.item = item;}T getItem() {return item;}
}class WithColor2<T extends HasColor>extends HoldItem<T> {WithColor2(T item) {super(item);}java.awt.Color color() {return item.getColor();}
}class WithColorCoord2<T extends Coord & HasColor>extends WithColor2<T> {WithColorCoord2(T item) {super(item);}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}class Solid2<T extends Coord & HasColor & Weight>extends WithColorCoord2<T> {Solid2(T item) {super(item);}int weight() {return item.weight();}
}public class InheritBounds {public static void main(String[] args) {Solid2<Bounded> solid2 =new Solid2<>(new Bounded());solid2.color();solid2.getY();solid2.weight();}
}
??????? Solid2變得更加簡潔了。
????????在這里,每一層的繼承都會為對應的類增加邊界的限制,同時繼承那些來自父類的方法。這樣我們就不需要在每個類中重復定義那些代碼了。
??????? 另外,創建泛型集合時需要注意,我們可以且只可以繼承一個接口或類:
// 可以進行的操作:
List<? extends Coord> list;
List<? extends HasColor> list1;
// 不可行的操作:
// List < ? extends HasColor & Weight > list2;
通配符
??????? 先看一個例子,將派生類的數組賦值給基類數組的引用:
【例子:數組的特殊行為】
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();// 但運行時的類型是Apple[],而不是Fruit[]或Orange[]try {// 編譯器允許添加Fruit(父類):fruit[0] = new Fruit();} catch (Exception e) { // 但這種操作卻會導致ArrayStoreException異常System.out.println(e);}try {// 編譯器允許添加Orange:fruit[0] = new Orange();} catch (Exception e) { // 但同樣會發生異常System.out.println(e);}}
}
??????? 程序執行的結果是:
??????? 在這個例子中,我們將派生類Apple的數組賦值給了Fruit數組:
Fruit[] fruit = new Apple[10];
這在繼承結構上是合理的。
??????? 不過需要注意一點,因為實際的數組類型是Apple[],所以將基類Fruit放入其中是不合理的。而編譯器允許了這個行為,因為從代碼上看,這只不過是將Fruit對象賦給了Fruit數組。數組機制能夠知道數組的實際類型,因此才會在運行時拋出異常。
??? 數組可以維持其包含的對象的類型規則,這也是為什么上例這種類似“向上轉型”的操作能夠成功的原因。它在一定程度上能夠確保我們不會亂用數組。
??????? 盡管我們能夠在運行時發現這種不合理的數組賦值。但使用泛型,我們可以在編譯時提前進行錯誤檢測:
【例子:泛型的編譯時檢查】
import java.util.ArrayList;
import java.util.List;public class NonCovariantGenerics {List<Fruit> flist = new ArrayList<Apple>();
}
??????? 編譯器會在編譯時發現如下的問題:
它告訴我們,我們無法將包含Apple的泛型賦值給包含Fruit的泛型。
??????? 之所以會這樣,是因為編譯器無法掌握足夠的信息,它并不知道List<Fruit>和List<Apple>是什么關系(另外,這種關系也不會涉及向上轉型,二者并不等價)。
??????? 可以發現,在這里我們需要討論的是集合自身的類型,而不是集合持有的元素類型。
??? 與數組不同,泛型并沒有內建的協變性。數組完全由語言自身定義,而泛型的定義卻來自于程序員。因此,編譯器和運行系統有足夠的信息來檢查數組,卻無法對泛型做到相同的事。
??????? 若一定需要在List<Fruit>和List<Apple>之間建立什么關系,可以使用通配符:
【例子:使用通配符建立關系】
import java.util.ArrayList;
import java.util.List;public class NonCovariantGenerics {public static void main(String[] args) {// 可以用通配符提供協變的能力:List<? extends Fruit> flist = new ArrayList<>();// 但卻不能添加任何類型的數據// flist.add(new Apple());// flist.add(new Fruit());// flist.add(new Object());flist.add(null); // 可以添加null,但沒什么用// 至少能返回一個Fruit對象:Fruit f = flist.get(0);}
}
??????? 顯然,這并不意味著flist真的會持有任何Fruit類型,因為<? extends Fruit>實際上表示的是“某種繼承自Fruit的類型”。這里存在著一個矛盾:
集合應該持有具體的類型,但flist只要求提供一種沒有被確切指定的類型。
換言之,flist所要求的類型并不具體(這是為了能夠向上轉型為flist做出的犧牲)。
??? 若一個集合并不要求所持有的類型足夠具體,這個集合就會失去意義。而若我們并不知道集合持有的具體元素是什么,我們也無法安全地向其中添加元素。
??????? 因為這種限制,通配符并不適合用于傳入參數的集合。但我們可以將其用于接收一個已經打包好的集合,并從中取出元素。
編譯器的能力范疇
??????? 按照上面的說法,若使用了通配符,我們似乎無法調用一個帶有參數的集合方法了。先看看這個例子:
【例子:調用泛型集合中的含參方法】
import java.util.Arrays;
import java.util.List;public class CompilerIntelligence {public static void main(String[] args) {List<? extends Fruit> flist =Arrays.asList(new Apple());Apple a = (Apple) flist.get(0); // 未產生警告// 方法中的參數是Object:flist.contains(new Apple());// 同樣,參數也是Object:flist.indexOf(new Apple());}
}
??????? 程序能夠順利執行。這似乎與之前得出的結論相悖——我們可以調用含參的集合方法。這是否是編譯器在其中進行調度呢?
??????? 答案是否定的,可以觀察contains()和indexOf()方法的參數列表:
contains()和indexOf()的參數都是Object的,假若我們調用了flist.add()方法,則會發現:
因為此時add()方法是參數已經變成了? extends Fruit。編譯器不會知道應該處理哪種具體的Fruit類型,因此不會接受任何類型。
??????? 這里體現了一種思路:作為泛型類的設計者,若我們認為某種調度是“安全的”,那么可以將Object作為其的參數。例如:
【例子:設置“安全”調度的參數】
import java.util.Objects;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;}// 使用Object作為參數@Overridepublic boolean equals(Object o) {return o instanceof Holder &&Objects.equals(value, ((Holder) o).value);}@Overridepublic int hashCode() {return Objects.hashCode(value);}public static void main(String[] args) {Holder<Apple> apple = new Holder<>(new Apple());Apple d = apple.get();apple.set(d);// 不允許這種操作:// Holder<Fruit> fruit = apple;// 但允許這種操作:Holder<? extends Fruit> fruit = apple;Fruit p = fruit.get();d = (Apple) fruit.get(); // 返回一個Object,然后再轉型try {Orange c = (Orange) fruit.get();} catch (Exception e) {System.out.println(e);}// 無法這樣調用set():// fruit.set(new Apple());// fruit.set(new Fruit());System.out.println(fruit.equals(d));}
}
??????? 程序執行的結果是:
??????? 可以看到,Holder<Apple>無法向上轉型為Holder<Fruit>,但卻可以向上轉型為Holder<? extends Fruit>。get()方法和set()方法的使用都會受編譯器的限制,值得一提的是,因為get()方法返回了Fruit,所以我們可以手動進行向下轉型。
??????? 因為equals()方法接受Object,所以它不會受到上述的限制。
逆變性
??????? 除extends之外,還可以使用超類通配符(重寫了super關鍵字)。如果說extends關鍵字可以為泛型添加限制,那么super就是為通配符添加了邊界限制,其中的邊界限制就是某個類的基類。例如:
<? super MyClass>
<? super T> // 可以使用類型參數
// <T super MyClass> // 但無法為泛型參數設置超類邊界
??????? 有了超類通配符,就可以向集合中進行寫操作了:
【例子:向泛型集合中進行寫操作】
import java.util.List;public class SuperTypeWildcards {static void writeTo(List<? super Apple> apples) {apples.add(new Apple());apples.add(new Jonathan());// 但不可以添加基類元素:// apples.add(new Fruit());}
}
??????? 我們可以向apples類型中添加Apple及其的子類型。但由于apples的下界是Apple,所以我們無法安全地先這個泛型集合中添加Fruit。
【例子:總結一下通配符】
import java.util.Arrays;
import java.util.List;public class GenericReading {static List<Apple> apples =Arrays.asList(new Apple());static List<Fruit> fruit =Arrays.asList(new Fruit());// 調用精確的類型:static <T> T readExact(List<T> list) {return list.get(0);}// 兼容各種調用的靜態方法:static void f1() {Apple a = readExact(apples);Fruit f = readExact(fruit);f = readExact(apples);}// 類的類型會在其實例化后確定:static class Reader<T> {T readExact(List<T> list) {return list.get(0);}}static void f2() {Reader<Fruit> fruitReader = new Reader<>();Fruit f = fruitReader.readExact(fruit);// fruitReader的參數類型是Fruit// 因此不會接受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> fruitReader =new CovariantReader<>();Fruit f = fruitReader.readCovariant(fruit);Fruit a = fruitReader.readCovariant(apples);}public static void main(String[] args) {f1();f2();f3();}
}
??????? f1()使用了一個靜態的泛型方法readExact()。從f1()中的調用可以看出,readExact()可以兼容不同的方法調用。因此,若可以使用靜態的泛型方法,則不一定需要使用到協變。
??????? 從f2()中可以看出,泛型類的對象會在被實例化時確定下來。因此fruitReader的類型參數被確定成了Fruit。
無界通配符
??????? 無界通配符<?>表示“一個泛型可以持有任何類型”,但在更多時候它是一種裝飾,告訴別人我考慮過Java泛型,并確定此處的這個泛型可以適配任何類型。
【例子:無界通配符的使用】
import java.util.HashMap;
import java.util.Map;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());assign1(new HashMap<>());assign2(new HashMap<>());assign3(new HashMap<>());}
}
??????? 第一次調用的assign3()會會發出警告,可以在編譯時添加-Xlint:unchecked來觀察這個警告:
編譯器似乎不會區分Map和Map<?, ?>。下面的例子會展示出一點區別:
【例子:無界通配符帶來的區別】
import java.util.ArrayList;
import java.util.List;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;}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());assign1(new ArrayList<>());assign2(new ArrayList<>());assign3(new ArrayList<>());// 兩種定義都被List<?>接受List<?> wildList = new ArrayList();wildList = new ArrayList<>();assign1(wildList);assign2(wildList);assign3(wildList);}
}
??????? 這段代碼也會觸發一些警告:
這里體現了編譯器對List<?>和List<? extends Object>在處理上的不同。
??????? 編譯器似乎并不關心List和List<?>之間有何區別,因此對它們的處理才會如此相同。然而,盡管這二者都可以被看做List<Object>,但在細節上它們仍有區別,它們實際的指代如下:
- List:實際上表示“持有任何Object類型的原生List”。
- List<?>:持有某種具體類型的非原生List(不過我們并不知道具體類型是什么)。
??????? 不過,在一些情況下,編譯器仍會區分二者:
【例子:區分不同的泛型】
public class Wildcards {static void rawArgs(Holder holder, Object arg) {// 會觸發警告:holder.set(arg);// 當前作用域中也沒有T,所以不能這樣寫:// T t = holder.get();// 可以這么寫,但會丟失類型信息:Object obj = holder.get();}// 與rawArgs()不同,方法會觸發報錯:static void unboundedArg(Holder<?> holder, Object arg) {// 發生報錯:// holder.set(arg);// 當然,這樣依舊不行:// T t = holder.get();// 可以,但還是會丟失類型信息:Object obj = holder.get();}static <T> T exact1(Holder<T> holder) {return holder.get();}static <T> T exact2(Holder<T> holder, T arg) {holder.set(arg);return holder.get();}static <T>T wildSubtype(Holder<? extends T> holder, T arg) {// 依舊會發生報錯:// holder.set(arg);return holder.get();}static <T>void wildSupertype(Holder<? extends T> holder, T arg) {// 引發報錯:// holder.set(arg);Object obj = holder.get();}public static void main(String[] args) {Holder raw = new Holder<>();// 這種寫法也一樣:raw = new Holder();Holder<Long> qualified = new Holder<>();Holder<?> unbounded = new Holder<>();Holder<? extends Long> bounded = new Holder<>();Long lng = 1L;rawArgs(raw, lng);rawArgs(qualified, lng);rawArgs(unbounded, lng);rawArgs(bounded, args);unboundedArg(raw, lng);unboundedArg(qualified, lng);unboundedArg(unbounded, lng);unboundedArg(bounded, lng);// 引發警告:Object r1 = exact1(raw);Long r2 = exact1(qualified);Object r3 = exact1(unbounded); // 方法返回Object類型// 引發異常:// Long r4 = exact1(bounded);// 引發警告Long r5 = exact2(raw, lng);Long r6 = exact2(qualified, lng);// 引發報錯:// Long r7 = exact2(unbounded, lng);// 同樣會報錯:// Long r8 = exact2(bounded, lng);// 引發警告:Long r9 = wildSubtype(raw, lng);Long r10 = wildSubtype(qualified, lng);// 同樣會獲得Object類型Object r11 = wildSubtype(unbounded, lng);// 引發異常:Long r12 = wildSubtype(bounded, lng);// 引發警告:wildSupertype(raw, lng);wildSupertype(qualified, lng);wildSupertype(bounded, lng);}
}
??????? 先看rawArgs中的holder.set(),編譯時會產生警告:
由于這里使用的是Holder的原始類型,所以任何向set()中傳入的類型都會被向上轉型為Object。編譯器知道這種行為是不安全的,所以發出了警告。注意:使用原始類型,就意味著放棄了編譯時檢查。
??????? 再看unboundedArg()中的holder.set(),與原生的Holder不同,使用Holder<?>時編譯器提示的警告級別是error:
這是因為原生的Holder可以持有任何類型的組合,而Holder<?>只能持有由某種具體類型組合成的單類型集合,因此我們無法傳入一個Object。
??????? 除此之外,exact1()和exact2()也因為參數的不同而受到了不同的限制:
可以看到,exact2()所受的限制更大。
??? 若向一個有“具體的”泛型類型(即無通配符)參數的方法中傳入原生類型,就會產生警告。這是因為具體參數所需的信息并不存在于原生類型中。
捕獲轉換
??????? <?>有一個特殊的用法:可以向一個使用了<?>的方法傳入原生類型,編譯器可能可以推斷出具體的類型參數。這被稱為捕獲轉換,通過這種方式,我們可以捕獲未指定的通配符類型,將其轉換成具體的類型:
【例子:捕獲轉換的使用例】
?
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); // 捕獲類型,并將具體的類型傳入f1()中}@SuppressWarnings("unchecked")public static void main(String[] args) {Holder raw = new Holder<>(1);// 若直接傳入f1()中,會產生警告f1(raw);// 但使用f2()就不會出現警告f2(raw);Holder rawBasic = new Holder();// 會產生警告:rawBasic.set(new Object());// 也不會出現警告f2(rawBasic);// 即使向上轉型為Holder<?>,依舊可以推斷出具體類型:Holder<?> wildcarded = new Holder<>(1.0);f2(wildcarded);}
}
??????? 程序執行的結果是:
??????? 需要注意的是,捕獲轉換經適用于“在方法中必須使用確切類型”的情況。我們無法從f2()方法中返回T,因為對f2()而言,T是未知的(因此,若需要返回值,我們需要自己傳入類型參數)。