<input id="0qass"><u id="0qass"></u></input>
  • <input id="0qass"><u id="0qass"></u></input>
  • <menu id="0qass"><u id="0qass"></u></menu>

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

    感謝大家和我一起,在Android世界打怪升級!

    泛型,一個所有人都知道怎么用,在JAVA世界老生常談的特性。更需要知其然,知其所以然。

    一、泛型是什么

    泛型是在JDK1.5引入的參數化類型特性,可以在同一段代碼上操作多種數據類型。

    1.1 參數化類型

    我們以泛型類的使用作為事例,如下:

    // 泛型類的定義
    public class Generics<T> {
        // 未知類型
        private T mData;
    
        public T getData() {
            return mData;
        }
    
        public void setData(T data) {
            this.mData = data;
        }
    }
    

    在泛型類內定義了泛型【T】,此時【T】是一個未知類型。

    // 泛型類的使用,將Person類作為參數傳入泛型類
    Generics<Person> generics = new Generics<Person>();
    

    在泛型類創建對象時,我們將Person類作為參數傳入泛型類,此時泛型類內部的【T】就變成了已知類型Person。

    通過參數傳入,作為泛型的類型,就是參數化類型。

    二、泛型種類及邊界

    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&B&…>,泛型重載了extends的關鍵字,與通常JAVA中使用的extends不同。

    • < T extends A>:單個邊界,A可以是類或接口,只能接收繼承或者實現A的類型。
    • < T extends A&B&…>:多個邊界,A可以是類或接口,A之后的只能是接口。比如:<T extends A&B&C>里面,T必須繼承A類型或實現A接口,并且必須實現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 代碼復用性強

    泛型就是使用參數化類型,在一段代碼上操作多種數據類型。比如:對幾個類的處理,在邏輯上完全相同,那自然會想這段邏輯代碼只寫一遍就好了,所以泛型就產生了。

    四、泛型的原理

    泛型在JDK1.5才出現,為了向下兼容,虛擬機是并不支持泛型的,所以JAVA在編譯階段除了進行類型判斷,還對泛型進行了擦除,于是所有的泛型在字節碼里都變成了原始類型,和C#的泛型不同,JAVA使用的是偽泛型。

    4.1 泛型擦除

    在編譯階段生成字節碼時,會進行泛型擦除,所以我們看下生成的字節碼文件,就可以清晰的看到泛型【T】被轉換成了Object。

    // java代碼
    public class Generics<T> {
        private T mData;
    
        public T getData() {
            return mData;
        }
    
        public void setData(T data) {
            this.mData = data;
        }
    }
    

    下面是Generics類生成的字節碼

    // 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被視為動態語言的關鍵。

    既然反射要知道所有的方法和屬性,但是泛型在字節碼里面被進行了擦除,那JAVA就使用備注的方式將泛型偷偷的寫入到了字節碼里面,保證反射的正常使用。

    4.2 泛型擦除原則

    • 如果泛型沒有限定(),則用Object作為原始類型。
    • 如果有限定(),則用A作為原始類型。
    • 如果有多個限定(<T extends A&B>),則使用第一個邊界A作為原始類型。

    五、泛型的限定通配符

    通配符是讓泛型的轉型更靈活。

    • <? extends A> 是指“上界通配符”
    • <? super A> 是指“下界通配符”

    5.1 通配符存在的意義

    數組是可以向上轉型的:

    Object[] nums = new Integer[2];
    nums[0] = 1;
    nums[1] = "string"; // nums在運行時是一個Interger數組,所以會報錯
    

    再看一段會報錯的泛型轉型代碼:

    // Apple extends Fruit,但是這樣轉型會報錯
    List<Fruit> fruits = new List<Apple>();
    

    由此可知,泛型的轉型和泛型類型是否繼承(Apple extends Fruit)沒有任何關系,泛型無法像數組一樣直接向上轉型,所以通配符的意義就是讓泛型的轉型更靈活。

    5.2 通配符詳解

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

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

    下次再見!

    公眾號

    下面是我的微信公眾號【老匡話Android】,所有的系列文章都會在公眾號同步更新,歡迎關注。

    相關推薦
    ??2020 CSDN 皮膚主題: 大白 設計師:CSDN官方博客 返回首頁
    多乐彩