萬用字元詳解

吾王彥發表於2021-04-30

前言

泛型的本質,其實就是將型別引數化,就是對於要操作的資料型別指定為一個引數。泛型,是為了在編譯的時候能檢測到非法的型別。而使用萬用字元,則是在此之上做的一個擴充套件,使泛型的使用更加的靈活。

泛型的好處

如果不是用泛型,想要對引數型別的“任意化”,就要做顯式的強制型別轉換。但這裡有個問題。請看一下程式碼。

public class Test{
	public static void main(String[] args) {
        
        showTest(); //不指定明確的型別,用Object
        showTest2(); //明確指定型別
    }
    
    //不指定明確的型別,用Object
    public static void showTest(){
        List<Object>  oblist = new ArrayList<>();
        oblist.add("abc");
        String  str =oblist.get(0);//這裡再編譯的時候不會出錯,但是在執行的時候就會報錯
        String  str2 =(String) oblist.get(0);//這裡做了顯式的強制型別轉換
        System.out.println(str);
        System.out.println(str2);
    }
    
    //明確指定型別
    public static void showTest2(){
        List<String>  oblist = new ArrayList<>();
        oblist.add("abc");
        String  str =oblist.get(0);//因為指定了型別,所以獲取到的值是不需要做型別轉換的
        System.out.println(str);
    }
}

從上面的額程式碼可看出, 省去了強制轉換,可以在編譯時候檢查型別安全。

萬用字元

常用的萬用字元有: T,E,K,V,?

其實也可以是A、B、C、D、E等的字母代替。使用 T,E,K,V,?只不過是約定俗成而已。

T,E,K,V,? 的約定如下:

T:(type) 表示具體的一個java型別。

E:代表Element。

K、V :分別代表java鍵值中的Key Value。

? :無界萬用字元,表示不確定的 java 型別

上邊界限定萬用字元 < ? extends E>

上邊界:用extends 關鍵字宣告,表示引數化的型別可能是所指定的型別,或者是此型別的子類。

有時候,為什麼要使用萬用字元,而不是簡單的泛型呢?其中有一個很重要的原因,就是使用萬用字元, 可以讓你的方法更具有通用性。

比如,有一個父類Animal,然後該父類有幾個子類。比如貓貓、狗等。它們都有名字的屬性,然後有一個動物列表。

你可以這樣寫:

List<Animal> animalList

也可以這樣寫

List<? extends Animal> animalList

如果想要獲取列表裡面的明細屬性,則:

//方式一
public static void getNameList(List< Animal > animals) {
	for (Animal animal : animals) {
		System.out.println(animal.getName());
	}
}

//方式二
public static void getNameList2(List<? extends Animal > animals) {	
	for (Animal animal : animals) {
		System.out.println(animal.getName());
	}
}
public static void main(String[] args) {
    Dog dog = new Dog();
	dog.setName("aa");
	List< Dog > dogs = new ArrayList<>();
	dogs.add(dog);
	getNameList(dogs);//報錯
	getNameList2(dogs);//不會報錯
}

方式二的入參寫法,限定了上界,但是不關心具體型別是什麼,所以對於傳入的型別是Animal、Animal的字類的都能支援,方式一則不行。

也可以使用<? extends Animal> 形式的萬用字元,實現向上轉型

向上轉型:

//Animal為一個父類,Dog為Animal的字類
Dog dog = new Dog(); //dog指向的物件在編譯時和執行時都是Dog型別
//下面的就是向上轉型
Animal dog = new Dog(); //dog指向的物件在編譯時是Animal型別,而執行時時Dog型別

使用<? extends Animal> 形式的萬用字元,實現向上轉型。

public class Test{
	public static void main(String[] args){
		List<? extends Animal> list = new ArrayList<Dog>();
		list.add(new Dog());  //不能新增,編譯報錯
		list.add(null);  //可以新增,不報錯。
		Animal animal = list.get(0); // 允許返回。
	}
}

這裡有個缺陷,不能對list做新增的操作,只能做讀取。

當使用extends萬用字元時,我們無法想list中新增任何東西(null除外),那又為什麼可以取出東西呢?

因為無論取什麼出來,我們都可以通過向上轉型用Animal指向它,這在Java中是被允許的,但不確定取到的是什麼,所以必須用上限接收。

Animal animal = list.get(0);//使用上限Animal接收。正確用法。
Dog animal = list.get(0); //錯誤

下邊界限定萬用字元 < ? super E>

又叫超型別萬用字元。與extends特性完全相反。

下邊界: 用 super 進行宣告,表示引數化的型別可能是所指定的型別,或者是此型別的父型別,直至 Object。

private <T> void test(List<? super T> dst, List<T> src){
    for (T t : src) {
        dst.add(t);
    }
}

public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = new ArrayList<>();
    new Test3().test(animals,dogs);
}

dst 型別 “大於等於” src 的型別,這裡的“大於等於”是指 dst 表示的範圍比 src 要大,因此裝得下 dst 的容器也就能裝 src 。

引數宣告

List<? super Dog> list = new ArrayList<Animal>(); //正確
List<? super Dog> list = new ArrayList<Object>();//因為 Object 是任何一個類的父級。正確

list元素的型別可以是任何Dog的父級,JVM在編譯的時候當然是無法確定具體是哪個型別,但是可以確定的是任何的Dog的子類都可以轉為Dog類,而任何的Dog的父類都不能轉為Dog類。

所以,若使用了super萬用字元,則只能存入T型別及T型別的字類物件:

list.add(new Dog());//可以新增
list.add(null);//編譯正常
list.add();//編譯錯誤bu
Dog dog = list.get(0); //錯誤
Animal dog = list.get(0);//錯誤
Object dog = list.get(0);//正確用法

取出資料的時候,JVM在編譯時並不能確定具體的父級,所以安全起見,就用頂級的父級Object來取出資料。這樣就可以避免發生強制型別轉換異常了。也只能使用Object取資料。

無邊界萬用字元

使用的形式是一個單獨的 ? ,表示無任何的限定。

List<?> list 表示 list 是持有某種特定型別的 List,但是不知道具體是哪種型別,因此時不安全的,即不能新增資料。但是可以用來取資料。

?和 T 的區別

先看一下程式碼:

//指定集合元素只能時T型別。d但是這個d
List<T>  list = new ArrayList<T>();

//表明集合的元素可以是任意的型別
List<?>  list = new ArrayList<?>();
public <T> void test(){
	List<T>  list = new ArrayList<T>();//指定集合元素只能是T型別,但是必須得配合方法使用,否則報錯
}
//表明集合的元素可以是任意的型別,沒什麼意義,一般在方法中只是為了說明用法
List<?>  list = new ArrayList<?>();

但是不管用T還是用? ,它們的共同點都是不能往list裡新增資料,且在獲取資料的時候只能用Object來接收。

其實,? 和T都是表示不確定的型別,區別在於我們可以對 T 進行操作,但是對 ?不行,比如如下這種 :

T t = operate();  //可以
? opa = operate();//不可以

T通常用於泛型類和泛型方法的定義。

通常用於泛型方法的呼叫程式碼和形參,不能用於定義類和泛型方法。

區別1:通過 T 來 確保 泛型引數的一致性

例如

//通過T來確保泛型引數的一致性。
public <T extends Number> void test(List<T> list1, List<T> list2){
    //......所以這裡的list集合的元素的型別是一致的
}

//萬用字元是 不確定的,所以這個方法不能保證兩個 List 具有相同的元素型別
public <? extends Number> void test2(List<? extends Number> list1,List<? extends Number> list2){
	//.....這裡的list的元素有可能一致,有可能不一致。
}

區別2:型別引數可以多重限定而?萬用字元不行

比如介面Apple繼承Fruits ,介面Fruit繼承Botany。下面就是T的多重限定的寫法。使用 &

public static <T extends Fluit & Apple> void testB(T t){
	//...
}

使用 & 符號設定多重邊界(Multi Bounds),指定泛型型別 T 必須是 Fluit和 Apple的共有子型別,此時變數 t 就具有了所有限定的方法和屬性。對於萬用字元來說,因為它不是一個確定的型別,所以不能進行多重限定。

區別3:萬用字元可以使用超類限定而型別引數不行

型別引數 T 只具有 一種 型別限定方式:

T extends A

但是萬用字元 ? 可以進行 兩種限定:

? extends A
? super A

總結

萬用字元的使用可以對泛型引數做出某些限制,使程式碼更安全,對於上邊界和下邊界限定的萬用字元總結如下:

使用萬用字元對泛型引數做出限制,能是程式碼更加的安全。

上下邊界限定的萬用字元總結如下:

  1. 使用 List<? extends C> list 的形式,表示該元素型別的範圍必須是 C 的字類( 包含 C 本身)。

  2. 使用 List<? super C> list 形式,表示該元素型別是 C 的超型別 ( 包含 C 本身 )。

相關文章