
感謝大家和我一起,在Android世界打怪升級!
泛型,一個所有人都知道怎么用,在JAVA世界老生常談的特性。更需要知其然,知其所以然。
在系列文章的前幾節(jié),我會和大家一起打牢JAVA的基礎,穿上布甲拿上木劍,披荊斬棘!
一、泛型是什么
泛型是在JDK1.5引入的參數(shù)化類型特性,可以在同一段代碼上操作多種數(shù)據(jù)類型。
1.1 參數(shù)化類型
我們以泛型類的使用作為事例,如下:
// 泛型類的定義
public class Generics<T> {
// 未知類型
private T mData;
public T getData() {
return mData;
}
public void setData(T data) {
this.mData = data;
}
}
在泛型類內定義了泛型【T】,此時【T】是一個未知類型。
// 泛型類的使用,將Person類作為參數(shù)傳入泛型類
Generics<Person> generics = new Generics<Person>();
在泛型類創(chuàng)建對象時,我們將Person類作為參數(shù)傳入泛型類,此時泛型類內部的【T】就變成了已知類型Person。
通過參數(shù)傳入,作為泛型的類型,就是參數(shù)化類型。
二、泛型種類及邊界
2.1 泛型種類
1. 泛型接口
public interface Base<T> {
public T getData();
public void setData(T data);
}
2. 泛型類
public class Generics<T>{
private T mData;
public T getData() {
return mData;
}
public void setData(T data) {
this.mData = data;
}
}
3. 泛型方法
// public后面的<T>是泛型方法的關鍵
public <T> Generics<T> getGenerics() {
return new Generics<T>();
}
2.2 泛型邊界
以上幾種類型均可定義泛型的邊界,語法 <T extends A>、<T extends A&B&...>,泛型重載了extends的關鍵字,與通常JAVA中使用的extends不同。
- < T extends A>:單個邊界,A可以是類或接口,只能接收繼承或者實現(xiàn)A的類型。
- < T extends A&B&...>:多個邊界,A可以是類或接口,A之后的只能是接口。比如:<T extends A&B&C>里面,T必須繼承A類型或實現(xiàn)A接口,并且必須實現(xiàn)B和C接口。
三、泛型的好處
3.1 代碼更健壯
泛型將集合的類型檢測提前到了編譯期,保證錯誤在編譯時就會拋出,基本上代碼編輯器(Android Studio、IDEA等)在書寫代碼階段給泛型傳入錯誤類型就會報錯。
在擁有泛型之前只能在運行時拋出類型轉換異常(ClassCastException),代碼十分脆弱。
// 泛型存在之前
// 集合里存入Fruit和Dog,編譯不會報錯
List fruits = new ArrayList();
fruits.add(new Fruit());
fruits.add(new Dog()); // X 錯誤的插入,直到運行時報錯
// 泛型存在之后
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Fruit());
fruits.add(new Dog());// X 編譯時就會報錯
3.2 代碼更簡潔
泛型省去了類型的強制轉換。在沒有泛型之前,集合內的對象都會被向上轉型為Object,所以需要強轉。
// 沒有泛型之前,獲取對象需要強轉
Fruit fruit = (Fruit) fruits.get(0);
3.3 代碼復用性強
泛型就是使用參數(shù)化類型,在一段代碼上操作多種數(shù)據(jù)類型。比如:對幾個類的處理,在邏輯上完全相同,那自然會想這段邏輯代碼只寫一遍就好了,所以泛型就產生了。
四、泛型的原理
泛型在JDK1.5才出現(xiàn),為了向下兼容,虛擬機是并不支持泛型的,所以JAVA在編譯階段除了進行類型判斷,還對泛型進行了擦除,于是所有的泛型在字節(jié)碼里都變成了原始類型,和C#的泛型不同,JAVA使用的是偽泛型。
4.1 泛型擦除
在編譯階段生成字節(jié)碼時,會進行泛型擦除,所以我們看下生成的字節(jié)碼文件,就可以清晰的看到泛型【T】被轉換成了Object。
// java代碼
public class Generics<T> {
private T mData;
public T getData() {
return mData;
}
public void setData(T data) {
this.mData = data;
}
}
下面是Generics類生成的字節(jié)碼
// class version 51.0 (51)
// access flags 0x21
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/kproduce/androidstudy/test/Generics<T>
public class com/kproduce/androidstudy/test/Generics {
// compiled from: Generics.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/lang/Object; mData
// access flags 0x1
public <init>()V
L0
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/kproduce/androidstudy/test/Generics; L0 L1 0
// signature Lcom/kproduce/androidstudy/test/Generics<TT;>;
// declaration: com.kproduce.androidstudy.test.Generics<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature ()TT;
// declaration: T getData()
public getData()Ljava/lang/Object;
L0
LINENUMBER 10 L0
ALOAD 0
GETFIELD com/kproduce/androidstudy/test/Generics.mData : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this Lcom/kproduce/androidstudy/test/Generics; L0 L1 0
// signature Lcom/kproduce/androidstudy/test/Generics<TT;>;
// declaration: com.kproduce.androidstudy.test.Generics<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void setData(T)
public setData(Ljava/lang/Object;)V
L0
LINENUMBER 14 L0
ALOAD 0
ALOAD 1
PUTFIELD com/kproduce/androidstudy/test/Generics.mData : Ljava/lang/Object;
L1
LINENUMBER 15 L1
RETURN
L2
LOCALVARIABLE this Lcom/kproduce/androidstudy/test/Generics; L0 L2 0
// signature Lcom/kproduce/androidstudy/test/Generics<TT;>;
// declaration: com.kproduce.androidstudy.test.Generics<T>
LOCALVARIABLE data Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: T
MAXSTACK = 2
MAXLOCALS = 2
}
看完上面的代碼有的同學就喊了,這不備注里面還是有泛型【T】嗎?

是的,你們說的沒錯!那為什么泛型還在備注里面?這時候要說到反射這個概念。
反射是在運行時對于任何一個類,都可以知道里面所有屬性和方法。對于任何一個對象,都可以調用它的方法和屬性。是JAVA被視為動態(tài)語言的關鍵。
既然反射要知道所有的方法和屬性,但是泛型在字節(jié)碼里面被進行了擦除,那JAVA就使用備注的方式將泛型偷偷的寫入到了字節(jié)碼里面,保證反射的正常使用。
4.2 泛型擦除原則
- 如果泛型沒有限定(<T>),則用Object作為原始類型。
- 如果有限定(<T extends A>),則用A作為原始類型。
- 如果有多個限定(<T extends A&B>),則使用第一個邊界A作為原始類型。
五、泛型的限定通配符
通配符是讓泛型的轉型更靈活。
- <? extends A> 是指“上界通配符”
- <? super A> 是指“下界通配符”
5.1 通配符存在的意義
數(shù)組是可以向上轉型的:
Object[] nums = new Integer[2];
nums[0] = 1;
nums[1] = "string"; // nums在運行時是一個Interger數(shù)組,所以會報錯
再看一段會報錯的泛型轉型代碼:
// Apple extends Fruit,但是這樣轉型會報錯
List<Fruit> fruits = new List<Apple>();
由此可知,泛型的轉型和泛型類型是否繼承(Apple extends Fruit)沒有任何關系,泛型無法像數(shù)組一樣直接向上轉型,所以通配符的意義就是讓泛型的轉型更靈活。
5.2 通配符詳解
-
上界通配符:<? extends Fruit>,F(xiàn)ruit是最上邊界,只能get,不能add。(詳解在代碼備注中)
public static void main(String[] args) {
List<GreenApple> greenApples = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
List<Food> foods = new ArrayList<>();
setData(greenApples);
setData(apples);
setData(foods); // 編譯錯誤,不在限制范圍內
}
public void setData(List<? extends Fruit> list){
// 上界通配符,只能get,不能add
// 【只能get】因為可以確保list被指定的對象一定可以向上轉型成Fruit
// 【不能add】因為設置的話無法確定是哪個子類,
// 有可能會將Banana設置到List<Apple>里面,所以不能set
Fruit fruit = list.get(0);
}
-
下界通配符:<? super Fruit>,F(xiàn)ruit是最下邊界,只能add,不能get。(詳解在代碼備注中)
public static void main(String[] args) {
List<Food> foods = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
setData(foods);
setData(apples); // 編譯錯誤,不在限制范圍內
}
public void setData(List<? super Fruit> list){
// 下界通配符,只能add,不能get
// 【只能add】因為可以確保list被指定的對象一定是Fruit的父類,
// 那Fruit的子類一定能向上轉型成對應的父類,所以可以add。
// 【不能get】因為被指定對象沒有固定的上界,不知道是哪個父類,所以無法精準獲取轉型成某一個類。
list.add(new Apple());
list.add(new Banana());
}
總結
最后咱們再總結一下泛型的知識點:
- 泛型是在JDK1.5引入的參數(shù)化類型特性。
- 泛型包括泛型接口、泛型類、泛型方法,可以使用<T extends A>設置邊界。
- 泛型可以使代碼更健壯(編譯期報錯)、代碼簡潔(不強轉)、復用性強。
- 泛型在JAVA中是偽泛型,虛擬機內不支持泛型類型,在編譯階段會進行泛型擦除,但是會留有備注給反射使用。
- 泛型的通配符讓轉型更加靈活。上界通配符只能get,不能add。下界通配符,只能add,不能get。
這樣泛型的介紹就結束了,希望大家讀完這篇文章,會對泛型有一個更深入的了解。如果我的文章能給大家?guī)硪稽c點的福利,那在下就足夠開心了。
下次再見!


