通配符詳解

前言

泛型的本質,其實就是將類型參數化,就是對於要操作的數據類型指定為一個參數。泛型,是為了在編譯的時候能檢測到非法的類型。而使用通配符,則是在此之上做的一個擴展,使泛型的使用更加的靈活。

泛型的好處

如果不是用泛型,想要對參數類型的「任意化」,就要做顯式的強制類型轉換。但這裡有個問題。請看一下代碼。

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 本身 )。

Tags: