Java泛型深入學習和總結 看這一篇就足夠!

未經允許禁止轉載,轉載請聯繫作者。

目錄

一 爲什麼我們需要泛型?

二 泛型類、泛型接口、泛型方法、泛型通配符

2.1 泛型類:

2.2 泛型接口:

2.3 泛型方法:

2.4 泛型通配符:

三 限定類型變量

四 泛型中的約束和侷限性

4.1 不能用基本類型實例化類型參數

4.2 運行時類型查詢只適用於原始類型

4.3 泛型類的靜態上下文中類型變量失效

4.4 不能創建參數化類型的數組

4.5 不能實例化類型變量

4.6 不能捕獲泛型類的實例

五 泛型類型的繼承規則

六 通配符類型

6.1 ? extends X

6.2  ? super X

6.3 無限定的通配符 ?

七 虛擬機是如何實現泛型的?


一 爲什麼我們需要泛型?

例子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”異常。

所以泛型的好處就是(現在看不懂沒關係,讀完回過來再品下):

  1. 適用於多種數據類型執行相同的代碼
  2. 泛型中的類型在使用時指定,不需要強制類型轉換

(可見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

https://www.cnblogs.com/xiaozhang9/p/6033939.html

https://juejin.im/post/5b28ca4e51882574e10df32c#heading-3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章