01、泛型是什么?——《Android打怪升級之旅》

感謝大家和我一起,在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());
}

總結

最后咱們再總結一下泛型的知識點:

  1. 泛型是在JDK1.5引入的參數(shù)化類型特性。
  2. 泛型包括泛型接口、泛型類、泛型方法,可以使用<T extends A>設置邊界。
  3. 泛型可以使代碼更健壯(編譯期報錯)、代碼簡潔(不強轉)、復用性強。
  4. 泛型在JAVA中是偽泛型,虛擬機內不支持泛型類型,在編譯階段會進行泛型擦除,但是會留有備注給反射使用。
  5. 泛型的通配符讓轉型更加靈活。上界通配符只能get,不能add。下界通配符,只能add,不能get。

這樣泛型的介紹就結束了,希望大家讀完這篇文章,會對泛型有一個更深入的了解。如果我的文章能給大家?guī)硪稽c點的福利,那在下就足夠開心了。

下次再見!


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容