在泛型概述-基本概念當中,我們介紹了有關型別引數限定的概念,使用 extends
關鍵字,給型別引數加以限定,例如:<T extends Fruit>
,它表示 Fruit
或者 Fruit
的子型別。
public class Plate<T extends Fruit> {
//......省略部分程式碼
public void addFruits(Plate<T> plate) {
for (T fruit : plate.getFruitList()) {
fruitList.add(fruit);
}
}
}
複製程式碼
在 Plate
類中,我們新增了一個方法 addFruits(Plate<T> plate)
,此方法的作用是將引數 plate,新增到當前的 Plate 物件當中。
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
Plate<Apple> applePlate = new Plate<>(appleList);
Plate<Fruit> fruitPlate = new Plate<>();
fruitPlate.addFruits(applePlate);
複製程式碼
我們構造了一個蘋果盤子 applePlate,並將它作為引數傳遞給 fruitPlate 的 addFruits() 方法,從實際生活的角度中來看,這樣的需求是沒有任何問題的,但是這麼寫的話,編譯器會報錯。因為 Plate<Fruit>
中的 addFruits(Plate<Fruit> plate)
的方法引數接收的是一個 Plate<Fruit>
型別的物件,而我們傳遞的確是一個 Plate<Apple>
型別的物件。
這個時候我們就需要對型別引數加以限定,來對 addFruits
改造一下:
public <E extends T> void addFruits(Plate<E> plate) {
for (T fruit : plate.getFruitList()) {
fruitList.add(fruit);
}
}
複製程式碼
我們對 addFruits
方法能夠接收的引數型別進行限定,它能夠接受的型別必須是 T
或者是 T
的子型別,這個時候我們上面的程式碼就可以正常執行了。
萬用字元
我們可以用一種更簡單的,帶子型別限定的萬用字元型別來替換上面的泛型方法。也就是將
public <E extends T> void addFruits(Plate<E> plate)
替換成 public void addFruits(Plate<? extends T> plate)
子型別限定
<? extends T>
,稱為子型別限定的萬用字元型別,它表示 T
以及 T
的任意子型別。採用萬用字元形式的寫法,無疑看上去更簡單明瞭。
這裡需要注意的是,萬用字元是用來例項化定義好的型別引數的。如果一個類或者一個方法並不是泛型類或者泛型方法,那我們是沒有辦法使用萬用字元的。
public class GenericType<?> {
? type;
public ? getType() {
return type;
}
public void setType(? type) {
this.type=type;
}
}
複製程式碼
GenericType 這種寫法是不支援的,可以看到,沒有辦法使用萬用字元型別來定義類,宣告屬性,也沒有辦法作為方法的返回型別。
子型別限定的萬用字元也有它的侷限性
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
List<? extends Fruit> fruitList = appleList;
for (Fruit fruit : fruitList) {
System.out.println(fruit.getName());
}
複製程式碼
使用了子型別限定萬用字元 <? extends Fruit>
,它表示 Fruit
以及 Fruit
的任意子型別。所以我們可以將 List<Apple>
型別的物件 appleList 賦值給它。還記得在泛型概述-基本概念當中有講過,泛型是不支援協變的,但是使用了這種子型別限定萬用字元型別之後,它就符合了協變的規則。
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//compile error
fruitList.add(new Apple("apple"));//compile error
fruitList.add(new Banana("banana"));//compile error
複製程式碼
我們可以看一下 ArrayList 類中的 add()
和 get()
兩個方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
複製程式碼
我們宣告瞭一個 List<? extends Fruit>
型別的物件 fruitList,那麼 add 方法和get 方法中的型別引數就變成了 ?extends Fruit 型別
public boolean add(? extends Fruit e) {
// 可以認為變成了這種形式,實際上沒有辦法這麼編寫
}
public ? extends Fruit get(int index) {
// 可以認為變成了這種形式,實際上沒有辦法這麼編寫
}
複製程式碼
當我們列印水果名稱的時候,也就是從 List<? extends Fruit>
型別的 fruitList 物件當中讀取資料的時候是沒有問題的,get()
方法返回的是 ? extends Fruit
型別,不論它是什麼型別,一定可以向上轉型成 Fruit 型別;但是當我們需要呼叫 add()
方法的時候,編譯器就會提示錯誤,這是因為 add()
方法的引數是 <? extends Fruit>
型別,它代表的是 Fruit
以及 Fruit
的任意子型別,並不能夠知道具體是什麼型別,所以就不能夠呼叫 add()
方法。add(null)
除外?
假設我們允許呼叫 add()
方法的話,就會出現問題
List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//假設沒有問題
fruitList.add(new Apple("apple"));//假設沒有問題
fruitList.add(new Banana("banana"));//假設沒有問題
複製程式碼
這就相當於是向 List<Apple>
型別的 appleList 物件中,新增了 Fruit
,Apple
,Banana
三種型別的物件,先不說它違背了泛型的型別安全的原則,這等於是埋下了一顆定時炸彈,在我們從 appleList
物件當中取資料的時候,就有可能發生型別轉換異常。
fruitList.add(new Apple("apple"));
複製程式碼
細心的朋友可能發現了,即使是向 fruitList 當中新增 Apple
型別的物件也是不可以的,上面說了 add 方法 的引數型別是 ? extends Fruit 對於編譯器來說它會將 ? extends Fruit 識別為 CAP#1 extends Fruit 型別,它沒有辦法匹配到 CAP#1的具體型別是什麼,所以沒有辦法進行賦值。
無限定萬用字元
<?>
稱為無限定萬用字元,當一些操作與具體的型別無關的時候,或者說我們不需要知道型別資訊的時候,就可以使用無限定的萬用字元型別,來例項化我們定義的型別引數,例如交換陣列中的元素,比較元素的大小,獲取元素的個數等等。
例如我們 Collections
工具類中提供的 swap 方法,用於交換列表中指定位置的元素,這個時候不需要知道具體的型別,可以使用萬用字元型別。
public static void swap(List<?> list, int i, int j) {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
複製程式碼
除了使用原生型別之外,我們還可以通過一個輔助的私有方法來匹配萬用字元的型別
public static void swap(List<?> list, int i, int j) {
swapHelp(list, i, j);
}
private static <T> void swapHelp(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
複製程式碼
對於編譯器來說它會將 ? 型別識別為 CAP#1 型別,在內部會呼叫 swapHelp 方法,編譯器會將推斷出 T
的型別就是 CAP#1,之後就像是普通的容器類一樣進行讀寫操作。
上面出現的關於萬用字元的例子,都可以使用泛型方法替代:
public static void swap(List<?> list, int i, int j)
複製程式碼
它的泛型方法形式是
public static <T> void swap(List<T> list, int i, int j)
複製程式碼
看到這裡你可能有些疑惑,因為 swap 方法的泛型方法形式和 swapHelp 方法的形式是一模一樣的,你可能會覺得這麼寫有一些多此一舉,採用萬用字元的形式簡潔明瞭,減少了引數型別的個數,所以應該儘可能的採用萬用字元型別。
並不是所有的泛型方法都可以改寫成萬用字元的形式,在這裡我直接引用老馬說程式設計中的兩個例子來說明:
引數型別間具有依賴關係
public static <D,S extends D> void copy(List<D> dest,
List<S> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
複製程式碼
我們只能將其簡化成具有一個引數型別的形式
public static <D> void copy(List<D> dest,
List<? extends D> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
複製程式碼
返回值依賴引數型別
public static <T extends Comparable<T>> T max(List<T> arr){
T max = arr.get(0);
for(int i=1; i<arr.size(); i++){
if(arr.get(i).compareTo(max)>0){
max = arr.get(i);
}
}
return max;
}
複製程式碼
max
方法的返回值是依賴於引數型別 T
的,也沒有辦法將其改寫為萬用字元形式。
超型別限定萬用字元
上面我們說到子類限定的萬用字元和無限定的萬用字元,兩種型別的萬用字元,這兩種萬用字元型別都有一個缺點,就是沒有辦法進行寫操作。
對於 List<?>
來說,我們獲取的元素只能賦值給 Object
型別的引用,並且沒有辦法進行 add 操作,除了 null
值。
對於 List<? extends T>
來說,我們獲取的元素只能賦值給它的上限 T
型別,同樣沒有辦法進行 add 操作,除了 null
值。
現在我們有這樣一個需求,需要將蘋果盤子中的水果,全部放到另一個水果盤子中區,也就是將 Plate<Apple>
中的所有水果,複製一份到 Plate<Fruit>
當中,這個需求沒有什麼問題,我們在 Plate<T extends Fruit>
類中加入一個方法:
public void copyTo(Plate<T> dest) {
List<T> destList = dest.getFruitList();
for (T t : fruitList) {
destList.add(t);
}
}
public static void main(String[] args) {
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
Plate<Apple> applePlate = new Plate<>(appleList);
Plate<Fruit> fruitPlate = new Plate<>();
applePlate.copyTo(fruitPlate);
for (Fruit fruit : fruitPlate.getFruitList()) {
System.out.println(fruit.getName());
}
}
複製程式碼
編譯器會在這一行 applePlate.copyTo(fruitPlate);
提示錯誤,copyTo 方法需要 Plate<Apple>
型別,但是我們傳遞的是 Plate<Fruit>
型別,這個時候就需要用到超型別限定的萬用字元:<? super T>
public void copyTo(Plate<? super T> dest) {
List<? super T> destList = dest.getFruitList();
for (T t : fruitList) {
destList.add(t);
}
複製程式碼
將 copyTo 方法改成這個樣子之後,就可以正常編譯執行了,對於 Plate<Apple>
的型別來說,它的 copyTo
方法接收的引數型別是 Plate<? super Apple>
型別,意思是 Apple
或者 Apple
的任意父型別,這個時候我們就可以將 Plate<Fruit>
型別的物件傳遞給該方法了。
另外超型別限定萬用字元允許寫入,詳細看一下 copyTo
方法中的程式碼,可以發現我們呼叫 destList.add(t)
方法將 T
型別的物件寫入到了 List<? super T>
的列表中,對於 <? super T>
而言,僅僅能夠寫入 T
或者 T
型別的子型別。
除了可以靈活的寫入之外,超型別限定的萬用字元型別還可以應用於實現 Comparable
介面
public class Fruit implements Comparable<Fruit> {
private int weight;
//... 省略部分程式碼
@Override
public int compareTo(Fruit fruit) {
if (weight == fruit.getWeight()) {
return 0;
} else if (weight > fruit.getWeight()) {
return 1;
} else {
return -1;
}
}
}
複製程式碼
讓 Fruit
實現 Comparable<Fruit>
介面,根據水果的重量 weight 的大小來作為比較規則
現在我們需要取出 Plate<T extends Fruit>
物件中重量最大的水果,程式碼如下:
public class PlateUtils {
public static <T extends Comparable<T>> T max(List<T> fruitList) {
Iterator<T> i = fruitList.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
}
List<Fruit> fruitList = new ArrayList<>();
Fruit fruit1 = new Fruit("fruit1");
fruit1.setWeight(2);
Fruit fruit2 = new Fruit("fruit2");
fruit2.setWeight(10);
fruitList.add(fruit1);
fruitList.add(fruit2);
Plate<Fruit> fruitPlate = new Plate<>(fruitList);
Fruit maxFruit = PlateUtils.max(fruitPlate.getFruitList());
System.out.println(maxFruit.getName());
//print:fruit2
複製程式碼
似乎沒有問題,但是如果我們將 List<Apple>
傳遞進去的話,編譯器就會報錯
public class Apple extends Fruit {
public Apple(String name) {
super(name);
}
}
複製程式碼
這是由於我們的 Apple 雖然繼承了 Fruit 但是並沒有實現 Comparable<Apple>
介面,對於 max 方法來說,它會根據引數推斷出 T
的實際型別為 Apple 型別,Apple 實現的是 Comparable<Fruit>
介面,然而需要的是 Comparable<Apple>
型別,型別不匹配,所以編譯器報錯。
對於 Apple 來說,它並不需要重新實現 Comparable<Apple>
介面,Fruit 類中實現的 compareTo 規則已經適用於它了,這個時候超型別限定萬用字元就派上用場了:
public static <T extends Comparable<? super T>> T max(List<T> fruitList) {
Iterator<T> i = fruitList.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
複製程式碼
我們修改了 T
型別的限定,限制它實現的 Comparable
介面型別必須是 T
或者 T
的超型別,對於 Apple
來說,它實現的是 Comparable<Fruit>
型別,符合要求。