未經允許禁止轉載,轉載請聯繫作者。
目錄
一 爲什麼我們需要泛型?
例子1:開發中,經常有數值類型求和的需求,例如實現int類型的加法, 有時候還需要實現long類型的求和, 如果還需要double類型的求和,需要重新在重載一個輸入是double類型的add方法。
public int addInt(int x,int y){
return x+y;
}
public float addFloat(float x,float y){
return x+y;
}
例子2:定義了一個List類型的集合,先向其中加入了兩個字符串類型的值,隨後加入一個Integer類型的值。這是完全允許的,因爲此時list默認的類型爲Object類型。在之後的循環中,由於忘記了之前在list中也加入了Integer類型的值或其他編碼原因,很容易出現類似於//1中的錯誤。因爲編譯階段正常,而運行時會出現“java.lang.ClassCastException”異常。因此,導致此類錯誤編碼過程中不易發現。
List<String> list = new ArrayList();
list.add("mark");
list.add("OK");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String name = list.get(i); // 1 這裏會報錯!因爲list中添加了一個int類型的100
System.out.println("name:" + name);
}
在如上的兩個中,我們發現主要存在兩個問題:
1.當我們將一個對象放入集合中,集合不會記住此對象的類型,當再次從集合中取出此對象時,改對象的編譯類型變成了Object類型,但其運行時類型任然爲其本身類型。
2.取出集合元素時需要人爲的強制類型轉化到具體的目標類型時,很容易出現“java.lang.ClassCastException”異常。
所以泛型的好處就是(現在看不懂沒關係,讀完回過來再品下):
- 適用於多種數據類型執行相同的代碼
- 泛型中的類型在使用時指定,不需要強制類型轉換
(可見2.3第一個代碼例子,形象!)
二 泛型類、泛型接口、泛型方法、泛型通配符
泛型,即“參數化類型”,就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(可以稱之爲類型形參),然後在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是爲了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數據類型被指定爲一個參數,這種參數類型可以用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。
引入一個類型變量T(其他大寫字母都可以,不過常用的就是T,E,K,V等等),並且用<>括起來,並放在類名的後面。泛型類是允許有多個類型變量的。
2.1 泛型類:
public class NormalGeneric<K> {
private K data;
public NormalGeneric(K data) {
this.data = data;
}
}
如何實現泛型接口的類呢:
1 :未傳入泛型實參時,在new出類的實例時,需要指定具體類型:
public class NormalGeneric<K> {
private K data;
public NormalGeneric(K data) {
this.data = data;
}
}
NormalGeneric<String> normalGeneric = new NormalGeneric<>();
2 :傳入泛型實參時:
public class NormalGeneric2 implements NormalGeneric<String> {
...
}
2.2 泛型接口:
public interface Genertor<T> {
public T next();
}
2.3 泛型方法:
public class GenericMethod {
public <T> T genericMethod(T ...a){
return a[0];
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
System.out.println(genericMethod.<String>genericMethod("mark","av","lance")); //1
System.out.println(genericMethod.genericMethod(12,34)); //2
System.out.println(genericMethod.genericMethod(true,false)); //3
}
}
//運行結果
//mark
//12
//true
//1處的String其實可以不寫。從//2和//3處可知,//2和//3在傳參時,就已經把參數的類型傳遞到了genericMethod(T ...a)方法中,賦給了T。
值得注意的是,泛型的本質是參數化類型,所以在確定類型的情況下,實際上並不是泛型方法,距離如下:
//這不是泛型方法,而是一個普通的方法,只是使用了Generic<Number>這個泛型類做形參而已。
//obj這個參數的類型已經被定死了,是已經確定的類型:Generic<Number>
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
//這也不是一個泛型方法,這也是一個普通的方法,只不過使用了泛型通配符?
//泛型通配符?是一種類型實參(具體介紹可見泛型通配符一節),可以看做爲所有類的父類
//也就是說,這裏的obj參數類型也是確定了的
public void showKeyValue2(Generic<?> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
2.4 泛型通配符:
Ingeter是Number的一個子類,類型擦除後Generic<Ingeter>與Generic<Number>實際上是相同的一種基本類型。那麼問題來了,在使用Generic<Number>作爲形參的方法中,能否使用Generic<Ingeter>的實例傳入呢?在邏輯上類似於Generic<Number>和Generic<Ingeter>是否可以看成具有父子關係的泛型類型呢?
爲了弄清楚這個問題,我們使用Generic<T>這個泛型類繼續看下面的例子:
public void showKeyValue(Generic<Number> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue這個方法編譯器會爲我們報錯:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
通過提示信息我們可以看到Generic<Integer>不能被看作爲Generic<Number>的子類。由此可以看出:同一種泛型可以對應多個版本(因爲參數類型是不確定的),不同版本的泛型類實例是不兼容的。
回到上面的例子,如何解決上面的問題?總不能爲了定義一個新的方法來處理Generic<Integer>類型的類,這顯然與java中的多臺理念相違背。因此我們需要一個在邏輯上可以表示同時是Generic<Integer>和Generic<Number>父類的引用類型。由此類型通配符應運而生。
我們可以將上面的方法改一下:
public void showKeyValue1(Generic<?> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
類型通配符一般是使用?代替具體的類型實參,注意了,此處’?’是類型實參,而不是類型形參 。再直白點的意思就是,此處的?和Number、String、Integer一樣都是一種實際的類型,可以把?看成所有類型的父類。是一種真實的類型。
可以解決當具體類型不確定的時候,這個通配符就是 ? ;當操作類型時,不需要使用類型的具體功能時,只使用Object類中的功能。那麼可以用 ? 通配符來表未知類型。
三 限定類型變量
有時候,我們需要對類型變量加以約束,比如計算兩個變量的最小,最大值。
public static <T> T min(T a,T b){
if(a.comapareTo(b)>0) return a; else return b;
}
請問,如果確保傳入的兩個變量一定有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的類的實例,將會發生編譯錯誤。
同時extends左右都允許有多個,如 T,V extends Comparable & Serializable
注意限定類型中,只允許有一個類,而且如果有類,這個類必須是限定列表的第一個。
這種類的限定既可以用在泛型方法上也可以用在泛型類上。
四 泛型中的約束和侷限性
現在我們有泛型類
public class Restrict<T>
4.1 不能用基本類型實例化類型參數
Restrict<Double> restrict = new Restrict<>();
解釋:因爲Restrict< Double> 類型擦除之後Restrict類含有Object類型,Object不能存儲double值。打個比方,如果你想放int的話,得寫integer,不能光一個int的。因爲int 是基本數據類型,Integer是其包裝類,注意是一個類,泛型是要寫個類的。(面試題)
4.2 運行時類型查詢只適用於原始類型
Restrict<String> restrictString= new Restrict<>();
System.out.println(restrict.getClass()==restrictString.getClass());
System.out.println(restrict.getClass().getName());
System.out.println(restrictString.getClass().getName());
運行結果:
true
cn.enjoyedu.generic.restrict.Restrict
cn.enjoyedu.generic.restrict.Restrict
因爲 類型擦除, java 虛擬機中的對象沒有泛型類型這一說, instanceof
和 getClass()
只能查詢到原始類型, 具體的泛型類型時無從得知的。
4.3 泛型類的靜態上下文中類型變量失效
不能在靜態域或方法中引用類型變量。因爲泛型是要在對象創建的時候才知道是什麼類型的,而對象創建的代碼執行先後順序是static的部分,然後纔是構造函數等等。所以在對象初始化之前static的部分已經執行了,如果你在靜態部分引用的泛型,那麼毫無疑問虛擬機根本不知道是什麼東西,因爲這個時候類還沒有初始化。
4.4 不能創建參數化類型的數組
Restrict<Double>[] restrictArray; //正確
Restrict<Double>[] restricts = new Restrict<Double>[10]; //錯誤,不能創建參數化類型的數組
只可聲明參數化類型的數組, 但不能創建參數化類型的數組。
4.5 不能實例化類型變量
public class Restrict<T> {
private T data;
//不能實例化類型變量
public Restrict() {
this.data = new T();
}
}
4.6 不能捕獲泛型類的實例
public class ExceptionRestrict {
/*泛型類不能extends Exception/Throwable*/
//private class Problem<T> extends Exception;
/*不能捕獲泛型類對象*/
// public <T extends Throwable> void doWork(T x){
// try{
//
// }catch(T x){
// //do sth;
// }
// }
/*這樣做可以*/
public <T extends Throwable> void doWorkSuccess(T x) throws T{
try{
}catch(Throwable e){
throw x;
}
}
}
五 泛型類型的繼承規則
現在我們有一個類和子類
public class Employee {
}
public class Worker extends Employee {
}
有一個泛型類
Employee employee = new Worker();
Pair<Employee> employeePair2 = new Pair<Worker>();
請問Pair<Employee>和Pair<Worker>是繼承關係嗎?
答案:不是,他們之間沒有什麼關係
但是泛型類可以繼承或者擴展其他泛型類,比如List和ArrayList
/*泛型類可以繼承或者擴展其他泛型類,比如List和ArrayList*/
private static class ExtendPair<T> extends Pair<T>{
}
Pair<Employee> pair = new ExtendPair<>();
六 通配符類型
2.4中講到了泛型通配符,用 ? 通配符來表未知類型,現在我們再來詳細描述下子類、超類、無限定通配符。
正如前面所述的,Pair<Employee>和Pair<Worker>沒有任何關係,現在如果我們有一個泛型類和一個方法
//方法
public static void print(GenericType<Fruit> p){
System.out.println(p.getData().getColor());
}
//泛型類
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
現在我們有繼承關係的類
public class Fruit extends Food {
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
public class Orange extends Fruit {
}
public class Apple extends Fruit {
}
public class HongFuShi extends Apple {
}
則會產生這種情況:
public static void use(){
GenericType<Fruit> a = new GenericType<>();
print(a);
GenericType<Orange> b = new GenericType<>();
print(b); //這行報錯 應爲方法print(GenericType<Fruit> p)中泛型的參數類型定爲了Fruit
}
爲解決這個問題,於是提出了一個通配符類型 ?
有兩種使用方式:
? extends X 表示類型的上界,類型參數是X的子類
? super X 表示類型的下界,類型參數是X的超類
6.1 ? extends X
表示傳遞給方法的參數,必須是X的子類(包括X本身)
public static void print2(GenericType<? extends Fruit> p){
System.out.println(p.getData().getColor());
}
public static void use2(){
GenericType<Fruit> a = new GenericType<>();
print2(a);
GenericType<Orange> b = new GenericType<>();
print2(b);
//print2(new GenericType<Food>());
GenericType<? extends Fruit> c = new GenericType<>();
}
但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法是不允許被調用的,會出現編譯錯誤
get方法則沒問題,會返回一個Fruit類型的值。爲何?
道理很簡單,? extends X 表示類型的上界,類型參數是X的子類,那麼可以肯定的說,get方法返回的一定是個X(不管是X或者X的子類)編譯器是可以確定知道的。但是set方法只知道傳入的是個X,至於具體是X的哪個子類,不知道。
總結:主要用於安全地訪問數據,可以訪問X及其子類型,並且不能寫入非null的數據。
6.2 ? super X
表示傳遞給方法的參數,必須是X的超類(包括X本身)
但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法可以被調用的,且能傳入的參數只能是X或者X的子類。
get方法只會返回一個Object類型的值。爲何?
? super X 表示類型的下界,類型參數是X的超類(包括X本身),那麼可以肯定的說,get方法返回的一定是個X的超類,那麼到底是哪個超類?不知道,但是可以肯定的說,Object一定是它的超類,所以get方法返回Object。編譯器是可以確定知道的。對於set方法來說,編譯器不知道它需要的確切類型,但是X和X的子類可以安全的轉型爲X。
總結:主要用於安全地寫入數據,可以寫入X及其子類型。
6.3 無限定的通配符 ?
表示對類型沒有什麼限制,可以把?看成所有類型的父類,如Pair< ?>;
比如:
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T類型
ArrayList<?> al=new ArrayList<?>();集合元素可以是任意類型,這種沒有意義,一般是方法中,只是爲了說明用法。
在使用上:
? getFirst() : 返回值只能賦給 Object,;
void setFirst(?) : setFirst 方法不能被調用, 甚至不能用 Object 調用;
七 虛擬機是如何實現泛型的?
泛型思想早在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泛型出現之前的寫法,泛型類型都變回了原生類型(Raw Type)。
JVM是運行字節碼的,而泛型是在編譯層面的,即泛型在編譯成字節碼之後到了JVM層面的話,就是普通類型了。
public static String method(List<String> stringList){
System.out.println("List");
return "OK";
}
public static Integer method(List<Integer> stringList){
System.out.println("List");
return 1;
}
上面這段代碼是不能被編譯的,因爲參數List<Integer>和List<String>編譯之後都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特徵簽名變得一模一樣。
由於Java泛型的引入,各種場景(虛擬機解析、反射等)下的方法調用都可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此,JCP組織對虛擬機規範做出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特徵簽名,這個屬性中保存的參數類型並不是原生類型,而是包括了參數化類型的信息。修改後的虛擬機規範要求所有能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。
另外,從Signature屬性的出現我們還可以得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們能通過反射手段取得參數化類型的根本依據。
參考文章:
https://blog.csdn.net/s10461/article/details/53941091
https://www.cnblogs.com/narojay/p/10812602.html