在定義類、接口和方法時,泛型使類型(類和接口)成為參數。與方法聲明中使用的形參非常相似,類型參數為您提供了一種方法,可以用不同的輸入重用相同的代碼。不同之處在于形式參數的輸入是值,而類型參數的輸入是類型。
使用泛型有許多好處:
1、在編譯時加強類型檢測
通過使用泛型,可以在編譯時捕獲和修復類型錯誤,從而避免在運行時出現 ClassCastException 等類型轉換異常。這提高了代碼的可靠性和穩定性。
2、消除類型轉換
使用泛型可以避免一些操作的強制類型轉換
3、提高代碼重用性
泛型可以使代碼更加通用,可以編寫一次代碼來處理多種類型的數據。
4、提高性能
泛型是在編譯時進行類型檢查的,因此可以避免在運行時進行類型轉換,從而提高了程序的性能。
泛型定義
泛型可以定義在類,接口和方法上。泛型使用 < >來指定泛型類型。
public class Aminal<T> {private T flag;public T getFlag() {return flag;}public Aminal(T x){this.flag = x;}public static void main(String[] args) {Aminal<String> a1 = new Aminal<>("a");Aminal<Integer> a2 = new Aminal<>(2);System.out.println(a1.getFlag());System.out.println(a2.getFlag());}
}
如上,類變量flag在class定義時候指定為泛型,在對應使用泛型變量flag的地方都需要使用泛型進行接收和傳遞。
常見的泛型類型標識:
E - Element(表示元素,常見于JDK的集合框架中)
K - Key(鍵)
V - Value(值)
N - Number(數字)
T - Type (類型)
S, U, V等 - 第2個、第3個、第4個類型
上面這些泛型類型標識只是一種約定,不會強制進行校驗,你也完全可以自定義,如下
public class Fruit<XXX> {private XXX price;XXX getPrice(){return price;}void setPrice(XXX price){this.price = price;}public static void main(String[] args) {Fruit<Double> f1 = new Fruit<>();f1.setPrice(2.1d);Fruit<Integer> f2 = new Fruit<>();f2.setPrice(5);}
}
上面使用XXX來表示類型,一樣可以正常編譯使用。
多個泛型
定義泛型時,可以指定多個泛型類型,多個之間使用,隔開
public class Pair<K,V> {K key;V value;public Pair(K k,V v){this.key = k;this.value = v;}public static void main(String[] args) {Pair<Integer,String> pair = new Pair<>(666,"泛型");System.out.println(pair.value);}
}
泛型方法
泛型方法相比于普通方法的聲明,還會在返回值前使用 < >來聲明使用的泛型參數列表
public static <T> List<T> fromArrayToList(T[] a) {return Arrays.stream(a).collect(Collectors.toList());
}
這里需要主要一點類上的泛型在類方法上都可以直接使用(注意是非靜態方法),不用在方法上聲明。
泛型類型限定
可以限定泛型為某個類的子類或實現了某個接口,使用extends來指定父類。
public static <T extends Number> float plus(T a ,T b){
return a.floatValue() + a.floatValue();
}
如果要限定多個條件可以使用 &來連接。
<T extends Number & Comparable>
通配符限定
在泛型代碼中,問號(?)被稱為通配符,用來表示未知類型。通配符可以在多種情況下使用:作為參數、字段或局部變量的類型;有時作為返回類型(盡管最好的編程實踐是更加具體)。通配符永遠不會被用作泛型方法調用的類型參數,泛型類的實例創建,或者超類型。
通配符限定只能使用在引用類型上,是是對泛型的限定。可以限定泛型的上界和下界。
<? extends Foo>
<? super Foo>
上界:? extends Foo表示泛型最高類型是Foo,只能是Foo及其子類。
下界:? super Foo表示泛型最低類型是Foo,只能是Foo及其父類。
無界:? 表示沒有類型限制
例如:
public void printFruits(List<? extends Fruit> fruits) {for (Fruit fruit : fruits) {System.out.println(fruit);}
}
這里入參約束成Fruit的上界,也就是入參只能是實現了Fruit接口的類,這樣在方法體中就可以調用Fruit接口統一的方法來完成邏輯操作。這里一定要理解 ?extends和 T extends的區別。?extends是針對引用類型,也就是實際參數,而T extends是方法或類的定義上。
類型擦除(Type Erasure)
Java 中的泛型在編譯時會進行類型擦除(Type Erasure)。類型擦除是 Java 泛型實現的一種機制,它允許你在編譯時使用泛型類型(也就是在編碼是進行檢測),但在運行時使用的是原始類型。在編譯時,泛型類型參數被擦除并替換為其邊界或 Object 類型。
驗證泛型擦除可以使用反射來操作class。
如下定義類
public class ErasureTest<T,X extends Number> {T t;X x;public void setT(T t){this.t = t;}
}
通過反射打印類信息:
Class c = ErasureTest.class;
for (Field field : c.getDeclaredFields()) {System.out.println(field.getName()+":"+field.getType());
}
for (Method method : c.getDeclaredMethods()) {System.out.println(method.getName()+":");Class[] params = method.getParameterTypes();for (int i = 0; i < params.length; i++) {System.out.println("參數"+params[i].getName()+",類型:"+params[i].getTypeName());
}
/**
輸出內容:
t:class java.lang.Object
x:class java.lang.Number
setT:
參數java.lang.Object,類型:java.lang.Object
*/
這里可以看到X extends Number轉換成了其上界Number,T轉換成了其原始類型Object。
擦除帶來的問題
由于擦除,通過反射在對方法進行調用時可以跳過類型約束:
List<String> list = new ArrayList<>();
list.add("haha");
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list,Integer.valueOf(1));
System.out.println(list);
如上代碼,定義了一個String型的list,但是我們通過反射成功的往list添加了一個Integer類型的,上面的代碼可以正常執行。這就可以繞過泛型限定。