簡介
-
說起各種高階語言,不得不談泛型,當我們在使用java集合的時候,會發現集合有個缺點:把一個物件“丟進”集合之後,集合就會“忘記”這個物件的資料型別,當再次取出該物件時,改物件的編譯型別就變成了
Object
型別- 問題1:集合對元素型別沒有任何限制,這樣可能會引發一些問題,比如建立一個用於儲存
A
物件的集合,但不小心把B
物件放進去,會引發異常 - 問題2: 由於把物件放進去時,集合對視了物件的狀態資訊,集合只知道它盛裝的是
Object
,因此去取集合元素後通常還需要進行強制型別裝換,這個過程不僅增加了程式設計的複雜度,還可能引發CLassCastException
異常
- 問題1:集合對元素型別沒有任何限制,這樣可能會引發一些問題,比如建立一個用於儲存
-
為解決以上問題,便引入“泛型”
-
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>
型別的變數,這種型變方式被稱為逆變
對於逆變的泛型而言,它只能呼叫泛型型別作為引數的方法;而不能呼叫泛型型別作為返回值型別的方法。口訣是:逆變只進不出
- 定義:萬用字元的下限用
- 定義:為了表示各種泛型List的父類,可以使用型別萬用字元,型別萬用字元是一個問號(?),將一個問號作為型別實參傳給List集合,寫作:
-
泛型方法
- 定義:所謂泛型方法,就是在宣告方法時定義一個或多個泛型形參,與類、介面中使用泛型引數不同的是,方法中的泛型引數無須顯式傳入實際型別引數
修飾符<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); }
- “菱形”語法前面已經提到,不再贅述,說一下啥是泛型構造器,其實就是java允許構造器簽名中宣告泛型形參
-
泛型方法與方法過載
- 因為泛型既允許設定萬用字元的上限,也允許設定萬用字元的下限,從而允許在一個類裡包含以下兩種方法的定義
<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 8改進了泛型方法的型別推斷能力,型別推斷主要有如下兩方面
-
泛型擦除和轉換
- 擦除:當把一個具有泛型資訊的物件賦給另一個沒有泛型資訊的變數時,所有在尖括號之間的型別資訊都將被扔掉;Java程式碼編譯成Class檔案, 然後再用位元組碼反編譯工具進行反編譯後, 將會發現泛型都不見了, 程式又變回了Java泛型出現之前的寫法, 泛型型別都變回了裸型別(
List<String>
對應的裸型別就是List
)- 比如:
List<String>
型別會被轉換成List,則該List對集合元素的型別檢查變成了泛型引數的上限(Object),那麼在使用,比如插入的時候,又會出現從Object到String的強制轉型程式碼 - 擦除法所謂的擦除, 僅僅是對方法的Code屬性中的位元組碼進行擦除, 實際上後設資料中還是保留了泛型資訊, 這也是我們在編碼時能通過反射手段取得引數化型別的根本依據
- 比如:
- java不支援原生型別的泛型,即是不支援 int/long等,
List<int>
這種是不支援的,那麼一旦把泛型資訊擦除後,遇到原生型別時把裝箱、 拆箱也自動做了,這也成為Java泛型慢的重要原因
- 擦除:當把一個具有泛型資訊的物件賦給另一個沒有泛型資訊的變數時,所有在尖括號之間的型別資訊都將被扔掉;Java程式碼編譯成Class檔案, 然後再用位元組碼反編譯工具進行反編譯後, 將會發現泛型都不見了, 程式又變回了Java泛型出現之前的寫法, 泛型型別都變回了裸型別(
-
泛型與陣列
- 陣列元素的型別不能包含泛型變數或泛型形參,除非是無上限的型別萬用字元,但可以宣告元素型別包含泛型變數或泛型形參的陣列。也就是說,只能宣告
List<String>[]
形式的陣列,但不能建立ArrayList<String>[10]
這樣的陣列物件
- 陣列元素的型別不能包含泛型變數或泛型形參,除非是無上限的型別萬用字元,但可以宣告元素型別包含泛型變數或泛型形參的陣列。也就是說,只能宣告
-
-
總結:Java的泛型在使用期間需要更加註意泛型擦除的情況,總體而言,其寫法也並不優雅。也希望未來的泛型會支援基本型別。