學習 Java,你不得不知的泛型知識

zzuhkp發表於2020-10-01

前言

泛型是 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 類作為類的原始型別抽象,然後又新增了一些其他的表示泛型的型別。如下圖所示。
Java 型別- 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 中泛型的簡化。

相關文章