1. 程式人生 > >【一天一個基礎系列】- java之泛型篇

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

### 簡介 + 說起各種高階語言,不得不談泛型,當我們在使用java集合的時候,會發現集合有個缺點:把一個物件“丟進”集合之後,集合就會“忘記”這個物件的資料型別,當再次取出該物件時,改物件的編譯型別就變成了`Object`型別 + 問題1:集合對元素型別沒有任何限制,這樣可能會引發一些問題,比如建立一個用於儲存`A`物件的集合,但不小心把`B`物件放進去,會引發異常 + 問題2: 由於把物件放進去時,集合對視了物件的狀態資訊,集合只知道它盛裝的是`Object`,因此去取集合元素後通常還需要進行強制型別裝換,這個過程不僅增加了程式設計的複雜度,還可能引發`CLassCastException`異常 + 為解決以上問題,便引入“泛型” + java 5以後,java引入了“引數化型別”的概念,允許程式在建立集合時指定集合元素的型別 + java 7之前,如果使用帶泛型的介面、類定義變數,那麼呼叫構造器建立物件時構造器的後面也必須帶泛型 + 比如 ``` //java 7之前 List list = new ArrayList();//後面的是必須帶上的 //java 7之後,"菱形"語法 List list = new ArrayList<>(); ``` > 注:java 9允許在使用匿名內部類時使用菱形語法 + 概念定義:允許在定義類、介面、方法時使用型別形參,這個型別形參將在宣告變數、建立物件、呼叫方式動態地指定 + 我們來看一下定義泛型介面、類 ``` /** * 定義泛型介面,實質:允許在定義介面、類時什麼型別形參, * 型別形參在整個介面、類體內可當成型別使用,幾乎所有可 * 使用普通型別的地方都可以使用這種型別形參 */ public interface List { void add(T x); } /** * 定義 * * */ @Data public class Clazz { private T a; public Clazz(T a){ this.a = a; } } //使用Clazz pulic void method(){ Clazz clazz = new Clazz<>(""); } ``` + 從泛型類派生子類 + 當建立了帶泛型宣告的介面、父類之後,可以為該介面建立實現類,或從該父類派生子類,需要指出的是,當使用這些介面、父類時不能再包含泛型形參 ``` //定義類Son類繼承Parent類 public class Son extends Parenet{ } //使用Parent類時為T形參傳入String型別 public class Son extends Parent{ } //使用Parent類時,沒有為T形參傳入實際的型別引數 public class Son extends Parent{ } ``` > 像這種使用Parent類時省略泛型的形式被稱為原始型別(raw type) > 如果從Parent類派生子類,則在Parent類中所有使用T型別的地方都將被替換成String型別 + 並不存在泛型類 + `List`與`List` 創建出來的是同樣class檔案,它們在執行時總有同樣的類,故在靜態方法、靜態初始化塊或者靜態變數的生命和初始化中不允許使用泛型形參 ``` public class R{ //錯誤,不能在靜態變數宣告中使用泛型形參 static T info; //錯誤,不能再靜態方法宣告中使用泛型形參 public void foo(T p){ } } ``` + 型別萬用字元 + 定義:為了表示各種泛型List的父類,可以使用型別萬用字元,型別萬用字元是一個問號(?),將一個問號作為型別實參傳給List集合,寫作:`List`(意思是元素型別未知的List)。這個問號(?)被稱為萬用字元,它的元素型別可以匹配任何型別 + 型別萬用字元的上限 + 定義:當直接使用`List·這種形式時,即表明這個List集合可以是任何泛型List的父類。但還有一種特殊的情形,程式不希望這個`List`是任何泛型List的父類,只希望它代表某一類泛型List的父類 ``` //定義上限為Parent類,表示泛型形參必須是Parent子類 List ``` + 協變:對於更廣泛的泛型類來說,指定萬用字元上限就是為了支援型別型變。比如Foo是Bar的子類,這樣`A`就相當於`A`的子類,可以將`A`賦值給`A`型別的變數,這種型變方式被稱為協變 + 型別萬用字元的下限 + 定義:萬用字元的下限用``的方式來指定,萬用字元下限的作用與萬用字元上限的作用恰好相反 ``` //定義下限為Parent類 List ``` + 逆變:比如Foo是Bar的子類,當程式需要一個`A`變數時,程式可以將`A`、`A`賦值給`A`型別的變數,這種型變方式被稱為逆變 > 對於逆變的泛型而言,它只能呼叫泛型型別作為引數的方法;而不能呼叫泛型型別作為返回值型別的方法。口訣是:逆變只進不出 + 泛型方法 + 定義:所謂泛型方法,就是在宣告方法時定義一個或多個泛型形參,與類、介面中使用泛型引數不同的是,方法中的泛型引數無須顯式傳入實際型別引數 ``` 修飾符返回值型別 方法名(形參列表){ //TODO } ``` + 泛型方法和型別萬用字元的區別 + 使用萬用字元比使用泛型方法(在方法簽名中顯式宣告泛型形參)更加清晰和準確 + 型別萬用字元既可以在方法簽名中定義形參的型別,也可以用於定義變數的型別;但泛型方法中的泛型形參必須在對應方法中顯式宣告 + 大多數時候都可以使用泛型方法來代替型別萬用字元 ``` //使用型別萬用字元 public interface Collection{ void add(Collection p); void delete(Collection p) } //使用泛型方法 public interface Collection{ void add(Collection p); void delete(Collection p) } ``` + 也可以同時使用泛型方法和萬用字元 ``` public class Collections{ public static void copy(List dest,List src){} } ``` + “菱形”語法與泛型構造器 + “菱形”語法前面已經提到,不再贅述,說一下啥是泛型構造器,其實就是java允許構造器簽名中宣告泛型形參 ``` class Foo{ public Foo(T t){ } } public void method(){ //泛型構造器中T型別為String new Foo(""); //也可以這麼定義,顯示指定T型別為String new Foo(""); //泛型構造器中T型別為Integer new Foo(10); } ``` + 泛型方法與方法過載 + 因為泛型既允許設定萬用字元的上限,也允許設定萬用字元的下限,從而允許在一個類裡包含以下兩種方法的定義 ``` void copy(Collection des,Collection src){}; T copy(Collection des,Collection src){}; ``` + 過載的情況 ``` public void method(List list){} public void method(List list){} ``` + 上述這段程式碼是不能被編譯的,因為引數`List`和`List`編譯之後都被擦除了, 變成了同一種的裸型別`List`,型別擦除導致這兩個方法的特徵簽名變得一模一樣(下面會提到型別擦除) + 型別推斷 + java 8改進了泛型方法的型別推斷能力,型別推斷主要有如下兩方面 + 1)可通過呼叫方法的上下文來推斷泛型的目標型別 + 2)可在方法呼叫鏈中,將推斷得到的泛型傳遞到最後一個方法 + **泛型擦除和轉換** + 擦除:當把一個具有泛型資訊的物件賦給另一個沒有泛型資訊的變數時,所有在尖括號之間的型別資訊都將被扔掉;Java程式碼編譯成Class檔案, 然後再用位元組碼反編譯工具進行反編譯後, 將會發現泛型都不見了, 程式又變回了Java泛型出現之前的寫法, 泛型型別都變回了裸型別(`List` 對應的裸型別就是`List`) + 比如:`List` 型別會被轉換成List,則該List對集合元素的型別檢查變成了泛型引數的上限(Object),那麼在使用,比如插入的時候,又會出現從Object到String的強制轉型程式碼 + 擦除法所謂的擦除, 僅僅是對方法的Code屬性中的位元組碼進行擦除, 實際上元資料中還是保留了泛型資訊, 這也是我們在編碼時能通過反射手段取得引數化型別的根本依據 + java不支援原生型別的泛型,即是不支援 int/long等,`List`這種是不支援的,那麼一旦把泛型資訊擦除後,遇到原生型別時把裝箱、 拆箱也自動做了,這也成為Java泛型慢的重要原因 + 泛型與陣列 + 陣列元素的型別不能包含泛型變數或泛型形參,除非是無上限的型別萬用字元,但可以宣告元素型別包含泛型變數或泛型形參的陣列。也就是說,只能宣告`List[]`形式的陣列,但不能建立`ArrayList[10]`這樣的陣列物件 + 總結:Java的泛型在使用期間需要更加註意泛型擦除的情況,總體而言,其寫法也並不優雅。也希望未來的泛型會支援基本