通配符詳解
前言
泛型的本質,其實就是將類型參數化,就是對於要操作的數據類型指定為一個參數。泛型,是為了在編譯的時候能檢測到非法的類型。而使用通配符,則是在此之上做的一個擴展,使泛型的使用更加的靈活。
泛型的好處
如果不是用泛型,想要對參數類型的「任意化」,就要做顯式的強制類型轉換。但這裡有個問題。請看一下程式碼。
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
總結
通配符的使用可以對泛型參數做出某些限制,使程式碼更安全,對於上邊界和下邊界限定的通配符總結如下:
使用通配符對泛型參數做出限制,能是程式碼更加的安全。
上下邊界限定的通配符總結如下:
-
使用
List<? extends C> list
的形式,表示該元素類型的範圍必須是C
的字類( 包含C
本身)。 -
使用
List<? super C> list
形式,表示該元素類型是C
的超類型 ( 包含C
本身 )。