1 為什么需要泛型
Java5之前,是沒有泛型的。通過兩段代碼我們就可以知道為何我們需要泛型
public int addInt(int a, int b) {return a + b;
}public double addDouble(double a, double b) {return a + b;
}
實際開發中,經常有數值類型求和的需求,例如實現int類型的加法,?有時候還需要實現long類型的求和,?如果還需要Float類型的求和,需要重新在重載一個輸入是Float類型的add方法。
再舉個例子:
public class NoGenericsTest {public static void main(String[] args) {List list = new ArrayList();list.add("Hello");list.add(123);list.add(45.67);for (int i = 0; i < list.size(); i++) {String item = (String) list.get(i); //1System.out.println(item);}}
}
上面的代碼執行,將會出現如下的錯誤:
定義了一個List類型的集合,先向其中加入了兩個字符串類型的值,隨后加入一個Integer類型的值。這是完全允許的,因為此時list默認的類型為Object類型。在之后的循環中,由于忘記了之前在list中也加入了Integer類型的值或其他編碼原因,很容易出現類似于//1中的錯誤。因為編譯階段正常,而運行時會出現“java.lang.ClassCastException”異常。因此,導致此類錯誤編碼過程中不易發現。
在如上的編碼過程中,我們發現主要存在兩個問題:
1.當我們將一個對象放入集合中,集合不會記住此對象的類型,當再次從集合中取出此對象時,改對象的編譯類型變成了Object類型,但其運行時類型任然為其本身類型。
2.因此,//1處取出集合元素時需要人為的強制類型轉化到具體的目標類型,且很容易出現“java.lang.ClassCastException”異常。
所以泛型的好處就是:
- 適用于多種數據類型執行相同的代碼
- 泛型中的類型在使用時指定,不需要強制類型轉換
2 泛型類和泛型接口
泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然后調用此方法時傳遞實參。那么參數化類型怎么理解呢?
顧名思義,就是將類型由原來的具體的類型參數化,類似于方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然后在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是為了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數據類型被指定為一個參數,這種參數類型可以用在類、接口和方法中,分別被稱為泛型類、泛型接口、泛型方法。
引入一個類型變量T(其他大寫字母都可以,不過常用的就是T,E,K,V等等),并且用<>括起來,并放在類名的后面。泛型類是允許有多個類型變量的.
2.1 泛型類
public class NormalGeneric<T> {private T value;public NormalGeneric(T value) {this.value = value;}public T getValue() {return value;}public void setValue(T value) {this.value = value;}@Overridepublic String toString() {return "NormalGeneric{" +"value=" + value +'}';}
}
package com.coy.generics;public class NormalGeneric2<T,K> {private T data;private K result;public NormalGeneric2(T t, K k) {this.data = t;this.result = k;}public T getData() {return data;}public void setData(T data) {this.data = data;}public K getResult() {return result;}public void setResult(K result) {this.result = result;}
}
泛型接口與泛型類的定義基本相同:
2.2 泛型接口
public interface Generator<T> {public T next();
}
而實現泛型接口的類,有兩種實現方法:
1. 未傳入泛型實參時:
public class ImplGenerator<T> implements Generator<T>{@Overridepublic T next() {return null;}
}
在new出類的實例時,需要指定具體類型:
public static void main(String[] args) {ImplGenerator<String> implGenerator = new ImplGenerator<>();
}
2. 傳入泛型實參
public class ImplGenerator2 implements Generator<String>{@Overridepublic String next() {return "OK";}
}
在new出類的實例時,和普通的類沒區別。
2.3 泛型方法
public class GenericMethod {public <T> T genericMethod(T... t) {// 這里可以對 T 類型的參數進行處理return t[t.length/ 2]; // 返回中間的元素作為示例}public void test(int x, int y) {// 這是一個普通方法,不是泛型方法System.out.println("普通方法: " + (x + y));}public static void main(String[] args) {GenericMethod gm = new GenericMethod();// 調用普通方法gm.test(12, 34);// 調用泛型方法String result = gm.genericMethod("Hello", "World", "Generics");System.out.println(result); // 輸出: WorldInteger intResult = gm.genericMethod(1, 2, 3, 4, 5);System.out.println(intResult); // 輸出: 3}
}
泛型方法,是在調用方法的時候指明泛型的具體類型?,泛型方法可以在任何地方和任何場景中使用,包括普通類和泛型類。注意泛型類中定義的普通方法和泛型方法的區別。
普通方法:
public class GenericNormalMethod<T> {/*** 普通泛型方法* 雖然在方法中使用了泛型,但是這并不是一個泛型方法。* 這只是類中一個普通的成員方法,只不過他的返回值是在聲明泛型類已經聲明過的泛型* 所以在這個方法中才可以繼續使用 т 這個泛型。** @param t 泛型參數* @return 返回泛型參數**/public T normalMethod(T t) {return t;}
}
泛型方法:
public class GenericGenericMethod {/*** 這才是一個真正的泛型方法。* 首先在public與返回值之間的<T>必不可少,這表明這是一個泛型方法,并且聲明了一個泛型* 這個τ可以出現在這個泛型方法的任意位置,* 泛型的數量也可以為任意多個* 如:public <T, V> T show(T t, V v) {}* * @param t* @return* @param <T>*/public <T> T show(T t) {return t;}public <T, V> T show(T t, V v) {return t;}
}
3 限定類型變量
有時候,我們需要對類型變量加以約束,比如計算兩個變量的最小,最大值。
public static <T> T min(T a, T b) {if (a instanceof Comparable && b instanceof Comparable) {Comparable<T> compA = (Comparable<T>) a;Comparable<T> compB = (Comparable<T>) b;return compA.compareTo(compB) < 0 ? a : b;}throw new IllegalArgumentException("Arguments must be comparable");
}
請問,如何確保傳入的兩個變量一定有compareTo方法?那么解決這個問題的方案就是將T限制為實現了接口Comparable的類
public static <T extends Comparable> T min(T a, T b) {if (a.compareTo(b) < 0) {return a;} else {return b;}
}
T extends Comparable中
T表示應該綁定類型的子類型,Comparable表示綁定類型,子類型和綁定類型可以是類也可以是接口。
如果這個時候,我們試圖傳入一個沒有實現接口Comparable的類的實例,將會發生編譯錯誤。
public static <T extends Comparable> T min(T a, T b) {if (a.compareTo(b) < 0) {return a;} else {return b;}
}public static <T extends Comparable & Serializable> T max(T a, T b) {if (a.compareTo(b) > 0) {return a;} else {return b;}
}static class Test{}public static void main(String[] args) {GenericGenericMethod.min("abc", "xyz"); // 輸出: abcInteger minInt = GenericGenericMethod.min(3, 5);System.out.println(minInt); // 輸出: 3Integer maxInt = GenericGenericMethod.max(3, 5);System.out.println(maxInt); // 輸出: 5Test test1 = new Test();Test test2 = new Test();Test minTest = GenericGenericMethod.min(test1, test2); // 報錯System.out.println(minTest); // 輸出: com.coy.generics.GenericGenericMethod$Test@<hashcode>
}
同時extends左右都允許有多個,如 <T,V?extends Comparable & Serializable>
注意:限定的類型中(Comparable & Serializable),只允許有一個是class類型,而且如果有class類型,這個class的類必須是限定列表的第一個,因為Java中類是單繼承。這種類的限定既可以用在泛型方法上也可以用在泛型類上。
4 泛型中的約束和局限性
現在我們有泛型類
public class Restrict<T> {
}
4.1 不能用基本類型實例化類型參數
Restrict<double> restrict = new Restrict<>(); // 錯誤
Restrict<Double> restrictDouble = new Restrict<>(); // 正確
Java 的泛型只支持引用類型,不支持基本類型(如 int、double 等)。這是因為泛型的實現基于類型擦除,編譯后泛型類型參數會被替換為 Object 或限定類型,而基本類型不能作為 Object 的子類。
例如,Restrict<int> 這樣的寫法會報錯,必須用包裝類型 Restrict<Integer>。
總結:
泛型類型參數只能是引用類型,不能是基本類型。
如果需要用基本類型,使用對應的包裝類(如 Integer、Double)
4.2 運行時類型查詢只適用于原始類型
if (restrictDouble instanceof Restrict<Double>) {} // 這種不允許
if (restrictDouble instanceof Restrict) {} // 這種不允許
Restrict<String> restrictString = new Restrict<>();
System.out.println(restrictDouble.getClass() == restrictString.getClass()); // This will print false because they are different type parameters
System.out.println(restrictDouble.getClass().getSimpleName()); // This will print "Restrict"
因為 Java 的泛型在編譯后會進行類型擦除,所有的泛型類型參數(如 Restrict<String>、Restrict<Integer>)在運行時都變成了原始類型(如 Restrict)。運行時虛擬機只知道原始類型,不知道具體的泛型參數類型。
所以,instanceof Restrict 是允許的,但 instanceof Restrict<String> 是非法的,因為運行時沒有 Restrict<String> 這個類型信息。
總結:
運行時只保留原始類型信息,泛型參數信息被擦除。
因此類型查詢(如 instanceof、getClass())只能用于原始類型。
4.3 泛型類的靜態上下文中類型變量失效
// 靜態域或者方法里不能引用類型變量
private static T instance; // 錯誤, 靜態成員不能直接引用類的泛型類型參數// 靜態方法不能引用類的類型變量
// private static T getInstance2() {} // 錯誤,靜態方法不能直接引用類的泛型類型參數//靜態方法本身是泛型方法就行
private static <T> T getInstance() {} // 正確,如果需要在靜態方法中使用泛型,必須讓方法本身成為泛型方法(即在方法聲明上加<T>),這樣T是方法級別的,與類的泛型參數無關。
不能在靜態域或方法中引用類型變量。泛型類型參數(如T)是在類實例化時由外部指定的,而靜態成員屬于類本身,不依賴于任何實例。類加載時,靜態成員就已經初始化了,但這時還沒有任何泛型類型信息(因為還沒創建對象)。同樣的靜態方法在類加載時隨類一起加載到方法區(或元空間),但只有在第一次主動使用該類(如創建對象、訪問靜態成員、調用靜態方法等)時,類才會被加載和初始化。靜態方法屬于類本身,不依賴于實例。所以靜態域或靜態方法不能直接引用類的泛型類型參數。
通俗點講因為泛型是要在對象創建的時候才知道是什么類型的,而對象創建的代碼執行先后順序為先是static的部分,然后才是構造函數等等。所以在對象初始化之前static的部分已經執行了,如果你在靜態部分引用的泛型,那么毫無疑問虛擬機根本不知道是什么東西,因為這個時候類還沒有初始化。
4.4 不能創建參數化類型的數組
// 不能創建參數化類型的數組
Restrict<Double>[] restrictArray; // 可以
// private Restrict<String>[] restrictArray = new Restrict<String>[10]; // 不允許
Java 的泛型是通過類型擦除實現的,編譯后泛型類型參數會被擦除為原始類型(如 Object)。而數組在運行時需要知道其元素的具體類型,以保證類型安全。如果允許創建參數化類型的數組(如 new Restrict<String>[10]),在運行時無法檢查數組元素的實際類型,可能導致類型安全問題。這樣做會破壞 Java 的類型安全機制,因此 Java 不允許直接創建參數化類型的數組。
總結:泛型信息在運行時不可用,數組需要運行時類型信息,所以不能創建參數化類型的數組。
4.5 不能實例化類型變量
public class Restrict<T> {private T value;// 不能實例化類型變量public Restrict() {value = new T();}
}
Java 的泛型是通過類型擦除實現的,類型參數 T 在編譯后會被擦除為它的限定類型(默認是 Object)。在運行時,JVM 并不知道 T 具體是什么類型,因此無法直接執行 new T() 這樣的操作,因為沒有類型信息來創建對象。
如果需要實例化類型變量,通常有兩種做法:
1. 通過構造函數傳入 Class<T>,用反射創建對象。
2. 讓調用者傳入需要的實例。
public class Restrict<T> {private T value;public Restrict(Class<T> clazz) throws IllegalAccessException, InstantiationException {value = clazz.newInstance();}
}
4.6 不能捕獲泛型類的實例
//泛型類不能extends Exception/Throwable// private class Problem<K> extends Exception{}// 不能捕獲泛型類對象
// public <T extends Throwable> void doSomething(T t) {
// // 不能捕獲泛型類對象
// try {
//
// } catch (T e) {
// // 這里的e是T類型,但T是Throwable的子類
// System.out.println("Caught: " + e);
// }
// }
Java 的泛型是通過類型擦除實現的,類型參數(如 T)在編譯后會被擦除為限定類型(如 Throwable 或 Object)。而 catch 語句在編譯時需要明確的異常類型,不能是類型變量。編譯器無法確定 T 具體是什么類型,因此不能寫 catch (T e)。
總結:
1. catch 語句要求捕獲的異常類型在編譯期已知。
2. 泛型類型參數在編譯期無法確定具體類型,類型擦除后只剩原始類型。
所以不能捕獲泛型類對象。
但是這樣是可以的:
public <T extends Throwable> void doSomething(T t) throws T {// 不能捕獲泛型類對象try {// 模擬一些操作throw t; // 拋出傳入的異常} catch (Throwable e) {// 這里的e是Throwable類型,但T是Throwable的子類System.out.println("Caught: " + e);throw (T) e; // 強制轉換為T類型并拋出}
}
5 泛型類型的繼承規則
現在我們有一個類和子類:
public class Employee {
}
public class Worker extends Employee{
}
有一個泛型類:
public class Pair<T> {
}
請問Pair<Employee>和Pair<Worker>是繼承關系嗎?
答案:不是。Pair<Employee> 和 Pair<Worker> 之間沒有繼承關系。雖然 Worker 繼承自 Employee,但泛型類型參數不同的泛型類是完全獨立的類型。在 Java 中,Pair<Worker> 不是 Pair<Employee> 的子類。
但是泛型類可以繼承或者擴展其他泛型類,比如List和ArrayList
/*** 但是泛型類可以繼承或者擴展其他泛型類,比如List和ArrayList* @param <T>*/
public class ExtendPair<T> extends Pair<T> {
}
Pair<Employee> pair = new ExtendPair<Employee>();
6 通配符類型
正是因為前面所述的,Pair<Employee>和Pair<Worker>沒有任何關系,
public class Fruit {
}public class Apple extends Fruit{
}public class Orange extends Fruit{
}public class HongFuShi extends Apple{
}
如果我們有一個泛型類和一個方法:
public static void print(GenericType<Fruit> generics) {System.out.println(generics);
}public class GenericType<T> {
}
則會產生這種情況:
為解決這個問題,于是提出了一個通配符類型??
有兩種使用方式:
? extends X??表示類型的上界,類型參數是X的子類
? super X??表示類型的下界,類型參數是X的超類
這兩種方式從名字上來看,特別是super,很有迷惑性,下面我們來仔細辨析這兩種方法。
6.1 ? extends X
表示傳遞給方法的參數,必須是X的子類(包括X本身)
public class Caller {public static void print2(GenericType<? extends Fruit> generics) {System.out.println(generics);}public static void use2() {GenericType<Fruit> a = new GenericType<>();print2(a);GenericType<Apple> b = new GenericType<>();print2(b);GenericType<? extends Fruit> c = b;}
}
但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法是不允許被調用的,會出現編譯錯誤
public class GenericType<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}@Overridepublic String toString() {return "GenericType{" +"value=" + value +'}';}
}
get方法則沒問題,會返回一個Fruit類型的值。
為何?
道理很簡單,? extends X??表示類型的上界,類型參數是X的子類,那么可以肯定的說,get方法返回的一定是個X(不管是X或者X的子類)編譯器是可以確定知道的。但是set方法只知道傳入的是個X,至于具體是X的那個子類,不知道。
總結:主要用于安全地訪問數據,可以訪問X及其子類型,并且不能寫入非null的數據。
6.2?? super?X
表示傳遞給方法的參數,必須是X的超類(包括X本身)
public class Caller {public static void printSuper(GenericType<? super Apple> generics) {System.out.println(generics);}public static void useSuper() {GenericType<Fruit> fruitGenericType = new GenericType<>();printSuper(fruitGenericType);GenericType<Apple> appleGenericType = new GenericType<>();printSuper(appleGenericType);GenericType<HongFuShi> hongFuShiGenericType = new GenericType<>();printSuper(hongFuShiGenericType); // 編譯錯誤,HongFuShi不是Apple的父類GenericType<Orange> orangeGenericType = new GenericType<>();printSuper(orangeGenericType); // 編譯錯誤,Orange不是Apple的父類}
}
但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法可以被調用的,且能傳入的參數只能是X或者X的子類:
public class GenericType<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}@Overridepublic String toString() {return "GenericType{" +"value=" + value +'}';}
}
public static void useSuper() {GenericType<? super Apple> e = new GenericType<>();Apple apple = new Apple();Fruit fruit = new Fruit();HongFuShi hongFuShi = new HongFuShi();e.setValue(fruit); // 編譯錯誤,不能將Fruit賦值給Apple的父類e.setValue(apple); // 可以將Apple賦值給Apple的父類e.setValue(hongFuShi); // 可以將HongFuShi賦值給Apple的父類
}
get方法只會返回一個Object類型的值:
GenericType<? super Apple> e = new GenericType<>();
Object fruit1 = e.getValue(); // 唯一可行的獲取值,返回類型為Object,因為e的類型是Apple的父類,所以getValue()返回的是Object類型
為什么會這樣子呢?
? super ?X??表示類型的下界,類型參數是X的超類(包括X本身),那么可以肯定的說,get方法返回的一定是個X的超類,那么到底是哪個超類?不知道,但是可以肯定的說,Object一定是它的超類,所以get方法返回Object。編譯器是可以確定知道的。對于set方法來說,編譯器不知道它需要的確切類型,但是X和X的子類可以安全的轉型為X。
總結:主要用于安全地寫入數據,可以寫入X及其子類型。
7?無限定的通配符 ?
表示對類型沒有什么限制,可以把?看成所有類型的父類,如Pair< ?>;
比如:
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T類型
ArrayList<?> al=new ArrayList<?>(); 集合元素可以是任意類型,這種沒有意義,一般是方法中,只是為了說明用法。
在使用上:
? getFirst() : 返回值只能賦給 Object;
void?setFirst(?) : setFirst 方法不能被調用, 甚至不能用 Object 調用;
8 虛擬機是如何實現泛型的?
泛型思想早在C++語言的模板(Template)中就開始生根發芽,在Java語言處于還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。由于Java語言里面所有的類型都繼承于java.lang.Object,所以Object轉型成任何對象都是有可能的。但是也因為有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什么類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會轉嫁到程序運行期之中。
泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有著根本性的分歧,C#里面泛型無論在程序源碼中、編譯后的IL中(Intermediate Language,中間語言,這時候泛型是一個占位符),或是運行期的CLR中,都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱為類型膨脹,基于這種方法實現的泛型稱為真實泛型。
Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節碼文件中,就已經替換為原來的原生類型(Raw Type,也稱為裸類型)了,并且在相應的地方插入了強制轉型代碼,因此,對于運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基于這種方法實現的泛型稱為偽泛型。
將一段Java代碼編譯成Class文件,然后再用字節碼反編譯工具進行反編譯后,將會發現泛型都不見了,程序又變回了Java泛型出現之前的寫法,泛型類型都變回了原生類型
public static String method(List<String> list) {return "List<String>";
}public static Integer method(List<Integer> list) {return 0;
}
上面這段代碼是不能被編譯的,因為參數List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特征簽名變得一模一樣。
由于Java泛型的引入,各種場景(虛擬機解析、反射等)下的方法調用都可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此,JCP組織對虛擬機規范做出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用于解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特征簽名[3],這個屬性中保存的參數類型并不是原生類型,而是包括了參數化類型的信息。修改后的虛擬機規范要求所有能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。
另外,從Signature屬性的出現我們還可以得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們能通過反射手段取得參數化類型的根本依據。