前言
泛型的本質,其實就是將型別引數化,就是對於要操作的資料型別指定為一個引數。泛型,是為了在編譯的時候能檢測到非法的型別。而使用萬用字元,則是在此之上做的一個擴充套件,使泛型的使用更加的靈活。
泛型的好處
如果不是用泛型,想要對引數型別的“任意化”,就要做顯式的強制型別轉換。但這裡有個問題。請看一下程式碼。
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
本身 )。