【一天一個基礎系列】- java之泛型篇

雙木l之林發表於2021-02-09

簡介

  • 說起各種高階語言,不得不談泛型,當我們在使用java集合的時候,會發現集合有個缺點:把一個物件“丟進”集合之後,集合就會“忘記”這個物件的資料型別,當再次取出該物件時,改物件的編譯型別就變成了Object型別

    • 問題1:集合對元素型別沒有任何限制,這樣可能會引發一些問題,比如建立一個用於儲存A物件的集合,但不小心把B物件放進去,會引發異常
    • 問題2: 由於把物件放進去時,集合對視了物件的狀態資訊,集合只知道它盛裝的是Object,因此去取集合元素後通常還需要進行強制型別裝換,這個過程不僅增加了程式設計的複雜度,還可能引發CLassCastException異常
  • 為解決以上問題,便引入“泛型”

    • java 5以後,java引入了“引數化型別”的概念,允許程式在建立集合時指定集合元素的型別

    • java 7之前,如果使用帶泛型的介面、類定義變數,那麼呼叫構造器建立物件時構造器的後面也必須帶泛型

      • 比如
      //java 7之前
      List<String> list = new ArrayList<String>();//後面的<String>是必須帶上的
      //java 7之後,"菱形"語法
      List<String> list = new ArrayList<>();
      
      

      注:java 9允許在使用匿名內部類時使用菱形語法

    • 概念定義:允許在定義類、介面、方法時使用型別形參,這個型別形參將在宣告變數、建立物件、呼叫方式動態地指定

    • 我們來看一下定義泛型介面、類

      /**
      * 定義泛型介面,實質:允許在定義介面、類時什麼型別形參,
      * 型別形參在整個介面、類體內可當成型別使用,幾乎所有可
      * 使用普通型別的地方都可以使用這種型別形參
      */
      public interface List<T> {
      	void add(T x);
      }
      
      /**
      * 定義
      * 
      * 
      */
      @Data
      public class Clazz<T> {
      	private T a;
      	public Clazz(T a){
      		this.a = a;
      	}
      }
      
      //使用Clazz
      pulic void method(){
      	Clazz<String> clazz = new Clazz<>("");
      }
      
    • 從泛型類派生子類

      • 當建立了帶泛型宣告的介面、父類之後,可以為該介面建立實現類,或從該父類派生子類,需要指出的是,當使用這些介面、父類時不能再包含泛型形參
      //定義類Son類繼承Parent類
      public class Son extends Parenet<T>{
      }
      
      //使用Parent類時為T形參傳入String型別
      public class Son extends Parent<String>{
      }
      
      //使用Parent類時,沒有為T形參傳入實際的型別引數
      public class Son extends Parent{
      }
      

      像這種使用Parent類時省略泛型的形式被稱為原始型別(raw type)
      如果從Parent類派生子類,則在Parent類中所有使用T型別的地方都將被替換成String型別

    • 並不存在泛型類

      • List<String>List<Integer> 建立出來的是同樣class檔案,它們在執行時總有同樣的類,故在靜態方法、靜態初始化塊或者靜態變數的生命和初始化中不允許使用泛型形參
      public class R<T>{
      	//錯誤,不能在靜態變數宣告中使用泛型形參
      	static T info;
      	//錯誤,不能再靜態方法宣告中使用泛型形參
      	public void foo(T p){
      	}
      }
      
    • 型別萬用字元

      • 定義:為了表示各種泛型List的父類,可以使用型別萬用字元,型別萬用字元是一個問號(?),將一個問號作為型別實參傳給List集合,寫作:List<?>(意思是元素型別未知的List)。這個問號(?)被稱為萬用字元,它的元素型別可以匹配任何型別
      • 型別萬用字元的上限
        • 定義:當直接使用List<?>·這種形式時,即表明這個List集合可以是任何泛型List的父類。但還有一種特殊的情形,程式不希望這個List<?>`是任何泛型List的父類,只希望它代表某一類泛型List的父類
        //定義上限為Parent類,表示泛型形參必須是Parent子類
        List<? extends Parent>
        
        • 協變:對於更廣泛的泛型類來說,指定萬用字元上限就是為了支援型別型變。比如Foo是Bar的子類,這樣A<Foo>就相當於A<? extends Bar>的子類,可以將A<Foo>賦值給A<? extends Bar>型別的變數,這種型變方式被稱為協變
      • 型別萬用字元的下限
        • 定義:萬用字元的下限用<? super型別>的方式來指定,萬用字元下限的作用與萬用字元上限的作用恰好相反
        //定義下限為Parent類
        List<? super Parent>
        
        • 逆變:比如Foo是Bar的子類,當程式需要一個A<? super Foo>變數時,程式可以將A<Bar>A<Object>賦值給A<? super Foo>型別的變數,這種型變方式被稱為逆變

        對於逆變的泛型而言,它只能呼叫泛型型別作為引數的方法;而不能呼叫泛型型別作為返回值型別的方法。口訣是:逆變只進不出

    • 泛型方法

      • 定義:所謂泛型方法,就是在宣告方法時定義一個或多個泛型形參,與類、介面中使用泛型引數不同的是,方法中的泛型引數無須顯式傳入實際型別引數
        修飾符<T,S>返回值型別 方法名(形參列表){
        	//TODO
        }
        
      • 泛型方法和型別萬用字元的區別
        • 使用萬用字元比使用泛型方法(在方法簽名中顯式宣告泛型形參)更加清晰和準確
        • 型別萬用字元既可以在方法簽名中定義形參的型別,也可以用於定義變數的型別;但泛型方法中的泛型形參必須在對應方法中顯式宣告
        • 大多數時候都可以使用泛型方法來代替型別萬用字元
          //使用型別萬用字元
          public interface Collection<E>{
          	void add(Collection<?> p);
          	void delete(Collection<? extends E> p)
          }
          
          //使用泛型方法
          public interface Collection<E>{
          	<T> void add(Collection<T> p);
          	<T extends E> void delete(Collection<T> p)
          }
          
        • 也可以同時使用泛型方法和萬用字元
          public class Collections{
          	public static <T> void copy(List<T> dest,List<? extends T> src){}
          }
          
    • “菱形”語法與泛型構造器

      • “菱形”語法前面已經提到,不再贅述,說一下啥是泛型構造器,其實就是java允許構造器簽名中宣告泛型形參
        class Foo{
        	public <T> Foo(T t){
        	}
        }
        
        public void method(){
        	//泛型構造器中T型別為String
        	new Foo("");
        	//也可以這麼定義,顯示指定T型別為String
        	new<String> Foo("");
        	//泛型構造器中T型別為Integer
        	new Foo(10);
        }
        
    • 泛型方法與方法過載

      • 因為泛型既允許設定萬用字元的上限,也允許設定萬用字元的下限,從而允許在一個類裡包含以下兩種方法的定義
        <T> void copy(Collection<T> des,Collection<? extends T> src){};
        <T> T copy(Collection<? super T> des,Collection<T> src){};
        
      • 過載的情況
        public void method(List<String> list){}
        public void method(List<Integer> list){}
        
        • 上述這段程式碼是不能被編譯的,因為引數List<Integer>List<String>編譯之後都被擦除了, 變成了同一種的裸型別List,型別擦除導致這兩個方法的特徵簽名變得一模一樣(下面會提到型別擦除)
    • 型別推斷

      • java 8改進了泛型方法的型別推斷能力,型別推斷主要有如下兩方面
        • 1)可通過呼叫方法的上下文來推斷泛型的目標型別
        • 2)可在方法呼叫鏈中,將推斷得到的泛型傳遞到最後一個方法
    • 泛型擦除和轉換

      • 擦除:當把一個具有泛型資訊的物件賦給另一個沒有泛型資訊的變數時,所有在尖括號之間的型別資訊都將被扔掉;Java程式碼編譯成Class檔案, 然後再用位元組碼反編譯工具進行反編譯後, 將會發現泛型都不見了, 程式又變回了Java泛型出現之前的寫法, 泛型型別都變回了裸型別(List<String> 對應的裸型別就是List)
        • 比如:List<String> 型別會被轉換成List,則該List對集合元素的型別檢查變成了泛型引數的上限(Object),那麼在使用,比如插入的時候,又會出現從Object到String的強制轉型程式碼
        • 擦除法所謂的擦除, 僅僅是對方法的Code屬性中的位元組碼進行擦除, 實際上後設資料中還是保留了泛型資訊, 這也是我們在編碼時能通過反射手段取得引數化型別的根本依據
      • java不支援原生型別的泛型,即是不支援 int/long等,List<int>這種是不支援的,那麼一旦把泛型資訊擦除後,遇到原生型別時把裝箱、 拆箱也自動做了,這也成為Java泛型慢的重要原因
    • 泛型與陣列

      • 陣列元素的型別不能包含泛型變數或泛型形參,除非是無上限的型別萬用字元,但可以宣告元素型別包含泛型變數或泛型形參的陣列。也就是說,只能宣告List<String>[]形式的陣列,但不能建立ArrayList<String>[10]這樣的陣列物件
  • 總結:Java的泛型在使用期間需要更加註意泛型擦除的情況,總體而言,其寫法也並不優雅。也希望未來的泛型會支援基本型別。

相關文章