學習 Java,你不得不知的泛型知識
前言
泛型是 Java 5 新增的一項特性,可以理解為型別的引數,主要用於程式碼重用,語義化程式碼,避免執行時的強制型別轉換異常。
在泛型出現之前,集合中的 List 儲存的物件只能為 Object,示例程式碼如下
List list = new ArrayList();
list.add("str");
Integer num = (Integer)list.get(0);
從 List 中獲取 Integer 型別的物件,需要進行強制型別轉換,如果不能保證儲存的物件只為 Integer 型別,很容易出現 ClassCastException。泛型出現後上述程式碼可修改為如下。
List<Integer> list = new ArrayList<>();
list.add("str"); // 編譯時報錯
Integer num = list.get(0);
修改後的程式碼中,List 型別後攜帶了<Integer>
,表示 List 中儲存的只能為 Integer 型別,此時如果向 List 中新增其他型別,則會在編譯時報錯,將執行時的型別檢查提前到編譯期,避免的錯誤的產生,同時語義也相對清晰,一眼可以看出 List 中儲存的是什麼型別。
泛型的使用
泛型類及泛型方法
使用泛型,首先需要進行定義,泛型可以用在類上和方法上。
泛型在類上面的定義,只需要在類後面新增尖括號,然後在尖括號中為泛型取一個名字即可,一般為比較簡短的大寫英文字母,常用的 T 表示任意型別,E 表示集合中的元素,K 和 V 分別表示鍵和值。示例如下。
public class GenericClazz<T> {
}
如果一個類中存在多個泛型,泛型的名稱之間可以用英文逗號分隔。示例如下。
public class GenericClazz<K,V> {
}
泛型可以用來表示任意型別,如果我們想要限制泛型的型別,則需要使用 extend 表示泛型只能為某個介面或類的子類。示例如下。
public class GenericClazz<T extend String> {
}
此時 T 只能用於表示字串型別,如果想表示多個介面的子類,則可以在型別之間使用 & 符合連線。示例如下。
public class GenericClazz<T extend String & Serializable> {
}
泛型定義後,一般我們會在成員變數或方法中使用,如下所示。
public class GenericClazz<T extend String> {
private T param;
public T getParam(){
return this.param;
}
public void setParam(T param){
this.param = param;
}
}
使用方式如下。
GenericClazz<String> clzss = new GenericClazz();
clazz.setParam("abc");
String param = clazz.getParam();
除了在類上定義泛型,還可以直接在方法上定義泛型。在方法上定義泛型需要在方法的修飾符後,返回值前定義泛型型別,如下所示。
public class Test {
public static <T> T getParam(T param) {
return param;
}
}
呼叫泛型方法的示例如下。
public class Test {
public static void main(String[] args) {
String param = Test.getParam("param");
}
}
型別擦除
每個泛型通過編譯都會轉換為一個原始型別,沒有 extend 限制的泛型對應的原始型別是 Object,有 extend 限制的泛型型別為 extend 後面的第一個型別。如下
public class GenericClazz<T extend String> {
private T param;
}
上述中的程式碼在編譯後可能會轉換為如下。
public class GenericClazz{
private String param;
}
也就是說,泛型是通過型別擦除實現的,編譯後的 class 檔案中泛型已經轉換為了具體的型別,由於存在型別擦除,編譯器可能會插入強制型別轉換的程式碼或生成橋接方法。
如下程式碼所示。
public class GenericClazz<T> {
private T param;
public T getParam() {
return this.param;
}
public static void main(String[] args) {
GenericClazz<Integer> clazz = new GenericClazz<>();
Integer param = clazz.getParam();
}
}
泛型型別 T 經過型別擦除,getParam 方法返回的型別會轉換為 Object 型別,示例程式碼將其返回值賦值給 Integer 型別的變數,因此編譯器會在賦值的指令中插入強制型別轉換的程式碼。
如果型別擦除和多型發生衝突,編譯器則會自動生成橋接方法,看下面的程式碼。
public class GenericClazz<T> {
private T param;
public T getParam() {
return this.param;
}
public void setParam(T param) {
this.param = param;
}
public static void main(String[] args) {
SubGenericClazz subGenericClazz = new SubGenericClazz();
subGenericClazz.setParam("str");
}
}
class SubGenericClazz extends GenericClazz<String> {
@Override
public void setParam(String param) {
return super.setParam(param);
}
}
不帶泛型的類 SubGenericClazz
繼承了泛型類 GenericClazz<String>
,然後實現其方法,然後將子類賦值給父類的引用,由於多型的存在,呼叫父類的方法時將會呼叫實際型別的方法,而父類由於型別擦除,最終呼叫的方法應該為 GenericClazz#setParam(Object param)
,而子類 SubGenericClazz
並不存在這樣的方法,此時型別擦除和多型發生衝突,編譯器自動生成橋接方法 SubGenericClazz#setParam(Object param)
,生成的程式碼可以理解如下。
class SubGenericClazz extends GenericClazz<String> {
// 生成的橋接方法
public void setParam(Object param){
return this.setParam((String)param);
}
@Override
public void setParam(String param) {
return super.setParam(param);
}
}
萬用字元型別
相同的型別,如果其泛型型別不同,則賦值會編譯失敗,如下所示。
GenericClazz<Number> genericClazz = new GenericClazz<Integer>();
這裡 GenericClazz<Number>
和 GenericClazz<Integer>
,雖然都是 GenericClazz
,但由於編譯時對泛型型別的檢查,因此會編譯失敗,為了解決這個問題,可以使用萬用字元型別。
萬用字元型別使用 ?
表示,對其型別的限制除了使用 extends,還可以使用 super。上述程式碼修正後如下。
GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();
extends 後面的表示萬用字元的上界,super 表示萬用字元的下界,如GenericClazz<? super Integer>
表示型別只能為 Integer 的父類,萬用字元如果存在上界或下界,將會影響包含萬用字元的物件的賦值,方法可傳入的引數型別、方法的的返回值型別等。
萬用字元設定上界示例程式碼如下。
GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();
Number param = genericClazz.getParam();
genericClazz.setParam(Integer.valueOf("1")); //編譯失敗
為萬用字元提供上界,則泛型型別作為返回值時只能返回上界的型別,而泛型型別則無法作為引數呼叫方法。
萬用字元設定下界示例程式碼如下。
GenericClazz<? super Integer> genericClazz = new GenericClazz<>();
Object param = genericClazz.getParam();
genericClazz.setParam(1);
為萬用字元設定下界後,泛型型別作為方法的返回型別只能返回 Object 型別,同時也只能使用萬用字元的下界型別作為方法引數的型別。
可以使用一個無上界和下界的萬用字元型別,此時和普通的泛型型別相比,泛型方法的返回值只能為 Object 型別,而萬用字元型別則無法作為方法的引數呼叫。
泛型與反射
雖然泛型通過型別擦除實現,但是編譯後的 class 檔案中仍保留著類或方法的泛型資訊,在前面的文章 Java 基礎知識之 Java 反射 主要將重點放在反射對型別的抽象上,反射同樣提供了獲取類的泛型資訊的能力。
泛型自 Java 5 誕生,為了描述泛型資訊,Java 將 Class 類作為類的原始型別抽象,然後又新增了一些其他的表示泛型的型別。如下圖所示。
- Type:表示 Java 的某一種型別。
- WidcardType:萬用字元型別,如
GenericClazz<? super Integer>
中的? super Integer
。 - Class:不包含泛型資訊的原始型別。
- ParameterizedType:引數化型別,如
public class GenericClazz<T extend Number> {}
中的GenericClazz<T extend Number>
。 - GenericArrayType:泛型陣列型別,如
T[]
。 - TypeVariable:型別變數,如
public class GenericClazz<T extend Number> {}
中的T extend Number
。
關於反射中有關泛型的 API ,使用示例如下所示。
Class<?> clazz = String.class;
// 獲取類的型別變數
TypeVariable<? extends Class<?>>[] typeParameters = clazz.getTypeParameters();
// 獲取類的泛型父類
Type genericSuperclass = clazz.getGenericSuperclass();
// 獲取類的泛型介面
Type[] genericInterfaces = clazz.getGenericInterfaces();
for (Method method : clazz.getDeclaredMethods()) {
// 獲取方法返回的泛型型別
Type genericReturnType = method.getGenericReturnType();
// 獲取引數的泛型型別
Type[] genericParameterTypes = method.getGenericParameterTypes();
}
TypeVariable<?> typeVariable = null;
// 獲取型別引數的子類限定
Type[] bounds = typeVariable.getBounds();
WildcardType wildcardType = null;
// 獲取萬用字元型別的上界
Type[] upperBounds = wildcardType.getUpperBounds();
// 獲取萬用字元型別的下界
Type[] lowerBounds = wildcardType.getLowerBounds();
ParameterizedType parameterizedType = null;
// 獲取引數化型別的原始型別
Type rawType = parameterizedType.getRawType();
// 獲取引數化型別中泛型的真實型別
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
GenericArrayType genericArrayType = null;
// 獲取泛型陣列的元素型別
Type genericComponentType = genericArrayType.getGenericComponentType();
總結
泛型是 Java 中的基礎知識,日常開發中,定義泛型類的場景相對較少一些,在集合中使用相對較多,泛型是學好 Java 必須掌握的技能,後面將介紹 Spring 對 Java 中泛型的簡化。
相關文章
- Java泛型知識Java泛型
- Java泛型知識點:泛型類、泛型介面和泛型方法Java泛型
- Java知識點總結(Java泛型)Java泛型
- java泛型學習(1)Java泛型
- 你不得不知道的MyBatis基礎知識之<resultMap>(4)MyBatis
- Java常見知識點彙總(⑪)——泛型Java泛型
- Java基礎知識掃盲(四)——泛型Java泛型
- Java不可不知的泛型使用Java泛型
- Android 你不得不學的HTTP相關知識AndroidHTTP
- Java隨筆-Java泛型的一點學習Java泛型
- 認識Java泛型Java泛型
- 不得不知道的golang知識點之nilGolang
- 你不可不知的50個數學知識
- 你不得不瞭解的HTML知識HTML
- 構建高效能佇列,你不得不知道的底層知識!佇列
- java入門基礎學習----泛型Java泛型
- java基礎學習之十三:泛型Java泛型
- 學HTML5開發,summary標籤的這些知識不得不知!HTML
- JAVA學習知識集合Java
- Java知識點學習Java
- IOS 開發不得不知道的網路知識iOS
- Java泛型複習Java泛型
- corejava基礎知識(3)-泛型Java泛型
- ?你必須知道的Java泛型Java泛型
- 你不得不知的Event LoopOOP
- Win8不得不知的動態磁貼小知識
- Java學習知識大綱Java
- java學習知識回顧Java
- java知識點學習圖Java
- 【暫定】你不可不知的50個生物學知識
- Kotlin知識歸納(十二) —— 泛型Kotlin泛型
- 做IT的,這些安全知識你不得不懂!
- 你不知道的記憶體知識記憶體
- 【java學習】java知識點總結Java
- JAVA泛型使用練習2(自定義泛型使用)Java泛型
- 菜鳥學Java(二十二)——重新認識泛型Java泛型
- C#泛型學習C#泛型
- 泛型學習筆記泛型筆記