泛型
什么是泛型
在強(qiáng)類型語言中,可以先不設(shè)置參數(shù)類型,用某個(gè)符號(hào)作為占位符.最后在運(yùn)行時(shí)指定參數(shù)類型來替換.
為什么要使用泛型
- 動(dòng)態(tài)化參數(shù),代碼編寫可以更加靈活、復(fù)用性高
- 類型安全,避免手動(dòng)的類型轉(zhuǎn)換.保證在運(yùn)行時(shí)出現(xiàn)的錯(cuò)誤能提早放到編譯時(shí)檢查
- 解耦類或方法所使用的類型之間的約束
java的泛型
要求
java的泛型是從jdk1.5之后引入的.所以使用泛型
的最低要求是jdk1.5
為什么會(huì)在jdk1.5之后引入泛型
最主要的原因就是為了重寫容器相關(guān)的類(Collection).如果沒有泛型,都使用Object來代替,那么就會(huì)出現(xiàn)大量的類型轉(zhuǎn)換與模板代碼,復(fù)用性低.
使用場(chǎng)景
- 暫時(shí)不指定類型,而是稍后再?zèng)Q定具體使用什么類型
- 限制其類型,使類型需要保持一直
- 大量的樣板重復(fù)代碼,只是類型不同
泛型語法
在類上編寫泛型
用<>表示泛型,T表示占位符類型,可以自定義不一定叫T.這樣就可以先不指定類型,最后在調(diào)用時(shí)指定.
- 示例
public class Holder<T> {
// 先不指定類型
private T value;
public void set(T val) {
value = val;
}
public T get() {
return value;
}
public static void main(String[] args) {
// 在調(diào)用時(shí)指定類型
Holder<String> holder = new Holder<>();
holder.set("test");
System.out.println(holder.get());
}
}
在方法上編寫泛型
摘自
Thinking in java
泛型設(shè)計(jì)的基本原則,如果泛型方法能夠代替泛型類,應(yīng)該盡量?jī)?yōu)先使用泛型方法.因?yàn)樵陬惿隙x泛型是全局的,在方法上定義作用在方法上,作用范圍更小.這樣就為類上預(yù)留了泛型參數(shù),適合以后擴(kuò)展.
- 示例
與類上定義泛型的語法不同的是,只要在返回值之前定義<T>,就能在方法上定義泛型
public class HolderUtils {
public static <T> T getHolder(Holder<T> holder) {
return holder.get();
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
System.out.println(HolderUtils.getHolder(holder));
}
}
泛型的工作原理
從上面的例子中看出,感覺java的泛型就是在運(yùn)行時(shí)指定類型就像 Holder<String> holder = new Holder<>();把指定的類型String 動(dòng)態(tài)的替換成占位符T.實(shí)際本非如此.在java中泛型參數(shù)類型都會(huì)轉(zhuǎn)成Object,擦除實(shí)際的類型.
- 下面是剛才例子的反編譯的結(jié)果
javap -c Holder.class
Compiled from "Holder.java"
public class generics.Holder<T> {
public generics.Holder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
// 傳入的T轉(zhuǎn)換成Object,類型被擦除
2: putfield #2 // Field value:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
// 返回的T也為Object,類型被擦除
1: getfield #2 // Field value:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class generics/Holder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String test
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_1
18: invokevirtual #8 // Method get:()Ljava/lang/Object;
// 需要時(shí)類型檢查,確保類型安全,然后強(qiáng)制類型轉(zhuǎn)換
21: checkcast #9 // class java/lang/String
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
}
從上面的結(jié)果可以看出泛型的工作原理為
- 編譯期檢查類型
- 編譯之后把實(shí)際類型替換成Object類型,參數(shù)實(shí)際類型
- 在需要時(shí),編譯器自動(dòng)先做類型檢查然后強(qiáng)制轉(zhuǎn)成需要的類型
java 泛型的局限性
java的泛型不是純粹的.在使用泛型時(shí),因?yàn)椴脸裏o法獲取具體的類型.不能使用泛型來執(zhí)行類型相關(guān)的例如
new instanceof
反射等操作.
// 因?yàn)楂@取不到真實(shí)的類型,轉(zhuǎn)成Object類型了,則會(huì)出現(xiàn)以下幾個(gè)問題
// 1. 泛型是不能new對(duì)象的
T t = new T(); // error
// 2. 泛型不能用instanceof
t instanceof // error
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
// 3.使用getClass().getTypeParameters() 只能獲取占位符 [T], 而不是實(shí)際的類型
System.out.println(Arrays.toString(
holder.getClass().getTypeParameters()));
}
什么是擦除
java的泛型是通過擦除來實(shí)現(xiàn)的.在使用泛型時(shí),任何類型信息都會(huì)被擦除.在泛型代碼內(nèi)部,無法獲得有關(guān)泛型參數(shù)類型的信息.只能獲取到定義泛型的占位標(biāo)識(shí)符
使用擦除的原因
java的泛型不是從jdk1.0就已經(jīng)存在的,泛型是從1.5開始的,需要向下兼容
老類庫需要升級(jí)泛型的兼容性.例如有x、y類庫, x 依賴于 y.
這時(shí)y升級(jí)使用了泛型,x沒有使用泛型.那么勢(shì)必不能對(duì)調(diào)用y的x產(chǎn)生影響.所以x應(yīng)該不具備感知y使用泛型的能力.所以當(dāng)依賴的類庫使用了泛型,則不能對(duì)現(xiàn)有類庫造成影響.所以泛型不是強(qiáng)制的,則類型信息必須被擦除.
如何解決擦除的問題
1. 定義泛型邊界
因?yàn)榉盒偷牟脸?我們獲取不到實(shí)際的類型,可以通過泛型邊界來獲取實(shí)際的類型.如果定義邊界,泛型將會(huì)擦除到第一個(gè)邊界
語法
使用extends關(guān)鍵字,來定義邊界,表示傳入的類型必須是其邊界的類型或者子類
例子:
public class HolderUtils {
public static <T extends Person> T getHolder(Holder<T> holder) {
T t = holder.get();
t.run();
return t;
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
// System.out.println(HolderUtils.getHolder(holder));
// error 類型不匹配,已經(jīng)限定了只能是Person以及Person的子類
Holder<Person> holderPerson = new Holder<>();
holderPerson.set(new Person());
System.out.println(HolderUtils.getHolder(holderPerson));
}
}
class Person {
public void run() {
System.out.println("run ...");
}
}
因?yàn)槎x了邊界所以擦除到第一個(gè)邊界類型Person.
這樣就不會(huì)使用Object來代替,就能夠使用邊界類型的方法與屬性.
解決了一部分的擦除問題
2. 傳入類型標(biāo)識(shí)
通過傳入類型標(biāo)識(shí)Class<T> tClass來確保泛型類型,
使用 class.newInstance()來創(chuàng)建對(duì)象,t就能確定其類型信息,
能夠執(zhí)行類型相關(guān)的操作例如instanceof等.這樣就徹底解決了擦除的帶來的類型問題.
public class HolderUtils {
public static <T extends Person> T getHolder(Holder<T> holder) {
T t = holder.get();
t.run();
return t;
}
public static <T> T newHolder(Class<T> tClass) throws IllegalAccessException, InstantiationException {
T t = tClass.newInstance();
if (t instanceof Person) {
System.out.println("type is Person...");
}
return t;
}
public static void main(String[] args) throws Exception{
Person person = newHolder(Person.class);
}
}
class Person {
public void run() {
System.out.println("run ...");
}
}
控制臺(tái)輸出:type is Person...
通配符
在講通配符之前,先要弄清2個(gè)知識(shí)點(diǎn)
- 什么是泛型容器類型
- 什么是泛型持有類型
泛型容器類型是可以自動(dòng)向上轉(zhuǎn)型的
// 泛型容器類型
List list = new ArrayList();
ArrayList -> List OK
表示ArrayList是List的某一種類型
泛型持有類型是不支持向上轉(zhuǎn)型的
class Fruit {}
class Apple extends Fruit {}
// 泛型持有類型
List<Fruit> fruit = new ArrayList<Apple>();
ArrayList -> List error 編譯不通過
雖然Apple能夠自動(dòng)向上轉(zhuǎn)型為Fruit,但是裝有蘋果的籃子不代表就能裝水果.當(dāng)泛型容器類型定義持有對(duì)象類型時(shí),容器類型就不能向上轉(zhuǎn)型.
為什么容器類型定義持有對(duì)象時(shí)就不能自動(dòng)向上轉(zhuǎn)型了
主要原因是會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤的問題,泛型的目的就是為了讓運(yùn)行期出現(xiàn)的問題,放到了編譯器處理.增加了代碼的健壯性,避免了類型轉(zhuǎn)換錯(cuò)誤.
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
// 數(shù)組可以編譯通過,但是在運(yùn)行期出現(xiàn)異常
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
try {
fruit[0] = new Orange();// ArrayStoreException
} catch(Exception e) { System.out.println(e); }
控制臺(tái)輸出: java.lang.ArrayStoreException: generics.Orange
// 而泛型直接編譯不通過,避免了運(yùn)行期的類型轉(zhuǎn)換錯(cuò)誤
List<Fruit> fruit = new ArrayList<Apple>(); // 編譯不通過
以上例子就說明了,當(dāng)裝有水果的籃子引用了只能裝蘋果籃子的實(shí)例,而去裝橘子時(shí),就會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤.而泛型的好處就是從運(yùn)行期的錯(cuò)誤提前到了編譯器.
那有什么辦法可以讓泛型容器在定義了持有對(duì)象時(shí)即能夠向上轉(zhuǎn)型又能夠保證類型安全呢叽掘?這時(shí)候就可以定義通配符
什么是通配符
通配符就是定義泛型容器的上下界,使其泛型容器類型可以做到類型安全的自動(dòng)類型轉(zhuǎn)換
通配符的語法
使用 <?> 來表示類型范圍
上界
使用 <? extend Fruit> 來表示上界.表示該集合都繼承于Fruit,都能返回Fruit的集合.所以讀取該集合的元素是類型安全的.添加元素是類型不安全的.
List<? extends Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // ok
Fruit apple = new Apple();
fruit.add(apple); // error
下界
使用 <? super Fruit> 來表示下界.表示該集合父類至少是Fruit的集合.所以添加元素是類型安全.讀取元素是類型不安全的,只能返回Object.
List<? super Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // error
Fruit apple = new Apple();
fruit.add(apple) // ok
無界
使用<?>來表示無界,表示該集合可以是任何類型,但是很遺憾,如果使用無界則不能添加元素,因?yàn)樵厥侨魏晤愋?在讀取時(shí)只能是Objcet,將丟失添加的類型,向下轉(zhuǎn)型有風(fēng)險(xiǎn).所以是類型不安全的.
List<?> list = new ArrayList<>();
Object o = list.get(0);
list.add(new String()); //error
list.add(new Object()); //error