一、泛型的概念
泛型是 Java SE5 出現(xiàn)的新特性,泛型的本質(zhì)是類型參數(shù)化或參數(shù)化類型,在不創(chuàng)建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型。
二、泛型的意義
一般的類和方法,只能使用具體的類型:要么是基本類型,要么是自定義的類。如果要編寫可以應用于多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。
Java 在引入泛型之前,表示可變對象,通常使用 Object 來實現(xiàn),但是在進行類型強制轉(zhuǎn)換時存在安全風險。有了泛型后:
- 編譯期間確定類型,保證類型安全,放的是什么,取的也是什么,不用擔心拋出 ClassCastException 異常。
- 提升可讀性,從編碼階段就顯式地知道泛型集合、泛型方法等處理的對象類型是什么。
- 泛型合并了同類型的處理代碼提高代碼的重用率,增加程序的通用靈活性。
舉個例子:
public static void method1() {
List list = new ArrayList();
List.add(22);
List.add("hncboy");
List.add(new Object());
for (Object o : list) {
System.out.println(o.getClass());
}
}
未使用泛型前,我們對集合可以進行任意類型的 add 操作,遍歷結(jié)果都被轉(zhuǎn)換成 Object 類型,因為不確定集合里存放的具體類型,輸出結(jié)果如下所示。
class java.lang.Integer
class java.lang.String
class java.lang.Object
采用泛型之后,創(chuàng)建集合對象可以明確的指定類型,在編譯期間就確定了該集合存儲的類型,存儲其他類型的對象編譯器會報錯。這時遍歷集合就可以直接采用明確的 String 類型輸出。
public static void method2() {
List<String> list = new ArrayList();
list.add("22");
list.add("hncboy");
//list.add(new Object()); 報錯
for (String s : arrayList) {
System.out.println(s);
}
}
三、泛型的表示
泛型可以定義在類、接口、方法中,分別表示為泛型類、泛型接口、泛型方法。泛型的使用需要先聲明,聲明通過<符號>的方式,符號可以任意,編譯器通過識別尖括號和尖括號內(nèi)的字母來解析泛型。泛型的類型只能為類,不能為基本數(shù)據(jù)類型。尖括號的位置也是固定的,只能在類名之后或方法返回值之前。
一般泛型有約定的符號:E 代表 Element,<E> 通常在集合中使用;T 代表 Type,<T >通常用于表示類;K 代表 Key,V 代表 Value,<K, V> 通常用于鍵值對的表示;? 代表泛型通配符。
泛型的表達式有如下幾種:
- 普通符號 <T>
- 無邊界通配符 <?>
- 上界通配符 <? extends E> 父類是 E
- 下界通配符 <? super E> 是 E 的父類
四、泛型的使用
4.1 泛型類
將泛型定義在類名后,使得用戶在使用該類時,根據(jù)不同情況傳入不同類型。在類上定義的泛型,在實例方法中可以直接使用,不需要定義,但是靜態(tài)方法上的泛型需要在靜態(tài)方法上聲明,不能直接使用。舉個例子:
public class Test<T> {
private T data;
public T getData() {
return data;
}
/** 這種寫法是錯誤的,提示 T 未定義 */
/*public static T get() {
return null;
}*/
/** 正確寫法,該方法上的 T 和類上的 T 雖然一樣,但是是兩個指代,可以完全相同,互不影響 */
public static <T> T get() {
return null;
}
public void setData(T data) {
this.data = data;
}
}
4.2 泛型方法
泛型方法,是在調(diào)用方法時指明的具體的泛型類型。雖然類上定義的泛型,實例方法中可以直接使用,但是該方法不屬于泛型方法。舉個例子:get 方法為泛型方法,而且該程序能編譯通過運行,因為尖括號里的每個元素都指代一種未知類型,可以為任何符號,尖括號里的 String 并非 java.lang.String 類型,而是作為泛型標識 <String>,傳入的 first 為 Integer 類型,所以該 String 標識符也指代 Integer 類型,返回值自然也是 Integer 類型。不過,應該也不會用這種泛型符號定義在實際情況中。
public class Test {
public static <String, T, Hncboy> String get(String string, Hncboy hncboy) {
return string;
}
public static void main(String[] args) {
Integer first = 666;
Double second = 888.0;
Integer result = get(first, second);
System.out.println(result);
}
}
4.3 泛型通配符
? 為泛型非限定通配符,表示類型未知,不用聲明,可以匹配任意的類。該通配符只能讀,不能寫,且不對返回值進行操作。也可以將非限定通配符出現(xiàn)的地方用普通泛型標識,不過使用通配符更簡潔。舉個例子:
test1() 是通過通配符來輸出集合的每一個元素的,test2() 和 test1() 的作用一樣,只不過將通配符用 <T> 來代替了;test3() 用來演示集合在通配符的情況下寫操作,發(fā)現(xiàn)編譯器報錯,int 和 String 都不屬于 ? 類型,當然放不進集合,因為所有類都有 null 元素,所以可以放進集合。比如主函數(shù)傳的是 List<Double>,而想要在集合里添加一個 String,這是不可能的;test4() 的寫法也是錯的,? 是不確定,返回值返回不了;test5() 的用法使用來比較 List<Object> 和 List<?> 的,在主函數(shù)里調(diào)用 test5(list) 報錯的,顯示 java: 不兼容的類型: java.util.List<java.lang.Integer>無法轉(zhuǎn)換為java.util.List<java.lang.Object>,因為 List<Integer> 不是 List<Object> 的子類。
public class Test {
public static void test1(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public static <T> void test2(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public static void test3(List<?> list) {
//list.add(1); capture of ?
//list.add("1"); capture of ?
list.add(null);
}
/*public static ? test4(List<?> list) {
return null;
}*/
public static void test5(List<Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
test1(list);
test2(list);
//test5(list);
}
}
通過使用泛型通配符可以實現(xiàn)泛型的上下邊界 <? extend T> 和 <? super T>,下面將使用 Number 類以及該類的子類來演示這兩種上下型邊界,Number 類的關(guān)系圖如下。

<? extends Number> 表示類型為 Number 或 Number 的子類,<? super Integer> 表示類型為 Integer 或 Integer 的父類,舉個例子,method1 方法測試是上邊界 Number,由于 arrayList1 和 arrayList2 的泛型都為 Number 或其子類,所以可以插入成功,而 arrayList3 的類型 String 和 Number 無關(guān),因此編譯報錯。method2 方法測試的是下邊界 Integer,由于 arrayList4,arrayList5 和 arrayList7 種的類型 Integer、Object 和 Number 都為 Integer 的父類,所以插入成功,而 arrayList7 的類型 Double,因此插入失敗。
public class Generic {
public static void main(String[] args) {
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<Number> arrayList2 = new ArrayList<>();
ArrayList<String> arrayList3 = new ArrayList<>();
method1(arrayList1);
method1(arrayList2);
//method1(arrayList3);
ArrayList<Integer> arrayList4 = new ArrayList<>();
ArrayList<Object> arrayList5 = new ArrayList<>();
ArrayList<Number> arrayList6 = new ArrayList<>();
ArrayList<Double> arrayList7 = new ArrayList<>();
method2(arrayList4);
method2(arrayList5);
method2(arrayList6);
//method2(arrayList7)
}
public static void method1(ArrayList<? extends Number> arrayList) {
}
public static void method2(ArrayList<? super Integer> arrayList) {
}
}
4.4 泛型接口
泛型接口就是在接口上定義的泛型,當一個類型未確定的類實現(xiàn)接口時,需要聲明該類型。舉個例子:
public interface CalcGeneric<T> {
T add(T num1, T num2);
}
public class CalculatorGeneric<T> implements CalcGeneric<T> {
@Override
public T add(T num1, T num2) {
return null;
}
}
4.5 泛型數(shù)組
數(shù)組是支持協(xié)變的,什么是數(shù)組的協(xié)變呢?舉個例子:這段代碼中,數(shù)組支持以 1 的方式定義數(shù)組,因為 Integer 是 Number 的子類,一個 Integer 對象也是一個 Number 對象,所以一個 Integer 的數(shù)組也是一個 Number 的數(shù)組,這就是數(shù)組的協(xié)變。雖然這種寫法編譯時能通過,但是數(shù)組實際上存儲的是 Integer 對象,如果加入 Double 對象,那么在運行時就會拋出 ArrayStoreException 異常,該種設計存在缺陷。3 方式所示的定義數(shù)組方式編譯錯誤,4 所指示的代碼才是正確的。泛型是不變的,沒有內(nèi)建的協(xié)變類型,使用泛型的時候,類型信息在編譯期會被類型擦除,所以泛型將這種錯誤檢測移到了編譯器。泛型的設計目的之一就是保證了類型安全,讓這種運行時期的錯誤在編譯期就能發(fā)現(xiàn),所以泛型是不支持協(xié)變的,如 5 所示的該行代碼會有編譯錯誤,
public class Test {
public static void main(String[] args) {
Number[] numbers = new Integer[10]; // 1
// java.lang.ArrayStoreException: java.lang.Double
numbers[0] = new Double(1); // 2
//List<String>[] list = new ArrayList<String>[10]; // 3
List<String>[] list2 = new ArrayList[10]; // 4
//List<Number> list3 = new ArrayList<Integer>(); // 5
}
}
4.6 泛型擦除
在泛型內(nèi)部,無法獲得任何有關(guān)泛型參數(shù)類型的信息,泛型只在編譯階段有效,泛型類型在邏輯上可看成是多個不同的類型,但是其實質(zhì)都是同一個類型。因為泛型是在JDK5之后才出現(xiàn)的,需要處理 JDK5之前的非泛型類庫。擦除的核心動機是它使得泛化的客戶端可以用非泛化的類庫實現(xiàn),反之亦然,這經(jīng)常被稱為"遷移兼容性"。
代價:泛型不能用于顯式地引用運行時類型地操作之中,例如轉(zhuǎn)型、instanceof 操作和 new 表達式,因為所有關(guān)于參數(shù)地類型信息都丟失了。無論何時,當你在編寫這個類的代碼的時候,提醒自己,他只是個Object。catch 語句不能捕獲泛型類型的異常。
舉個例子:這串代碼的運行輸出是,因此可見泛型在運行期間對類型進行了擦除。
class java.util.ArrayList
class java.util.ArrayList
true
public static void method1() {
List<Integer> integerArrayList = new ArrayList();
List<String> stringArrayList = new ArrayList();
System.out.println(integerArrayList.getClass());
System.out.println(stringArrayList.getClass());
System.out.println(integerArrayList.getClass() == stringArrayList.getClass());
}
將上面的 Java 代碼編譯成字節(jié)碼后查看也可看見兩個集合都是 java/util/ArrayList
public static method1()V
L0
LINENUMBER 14 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 0
L1
LINENUMBER 15 L1
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1
因為在運行期間類型擦除的關(guān)系,可以通過反射在運行期間修改集合能添加的類,不過添加后查詢該集合會拋出 ClassCastException 異常,代碼如下。
public static void method4() throws Exception {
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hnc");
stringArrayList.add("boy");
System.out.println("之前長度:" + stringArrayList.size());
// 通過反射增加元素
Class<?> clazz = stringArrayList.getClass();
Method method = clazz.getDeclaredMethod("add", Object.class);
method.invoke(stringArrayList, 60);
System.out.println("之后長度:" + stringArrayList.size());
// 存的還是 Integer 類型
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
for (int i = 0; i < stringArrayList.size(); i++) {
System.out.println(stringArrayList.get(i).getClass());
}
}
五、總結(jié)
泛型在平時的學習中用到的還是挺多的。
- 數(shù)組不支持泛型
- 泛型的類型不能為基礎數(shù)據(jù)類型
- 泛型只在編譯階段有效
Java 編程思想
碼出高效 Java 開發(fā)手冊