Java泛型總結

Java泛型總結#

泛型是什么##

從本質上講,泛型就是參數化類型。泛型十分重要,使用該特性可以創建類、接口以及方法,并且可以使用類型參數來指定類型。沒有泛型以前,對于不確定的類型常常使用Object類,因為Object類是其他任何類的超類,可以創建任何類型的對象。但是缺點也十分明顯,不能以類型安全的形式工作。泛型提供了以前沒有的類型安全性,簡化處理過程,不需要像使用Object類時需要類型轉換,泛型提供自動的隱式類型轉換

一個簡單的泛型實例##

創建Gen泛型類,類型參數為T。T可以是任何引用類型,使用時,可以傳遞任何引用類型。T在此處相當于就是一個占位符,其可以是任意引用類型。

public class Gen <T> {
    T ob;
    Gen(T ob){
        this.ob = ob;
    }

    T getOb(){
        return ob;
    }

    //顯示類型參數的類型
    void showType(){
        System.out.println("T's Type is: " + ob.getClass().getName());
    }
}

使用泛型,用具體的類型替換占位符T。

public class GenDemo {
    public static void main(String[] args) {
        Gen<String> strGen = new Gen<>("string type");
        strGen.showType();

        Gen<Integer> intGen = new Gen<>(100);
        intGen.showType();
    }
}

執行結果:

T's Type is: java.lang.String
T's Type is: java.lang.Integer

Process finished with exit code 0

可以看出,T的類型就是在使用泛型時傳遞的類型參數的類型。

Gen<String> strGen = new Gen<>("string type");

上面的語句將表示T的類型為String,此語句也可以聲明為下面的形式,但是上面的形式更加簡潔,從jdk 1.7開始可以去掉構造器中的類型參數,泛型提供了類型推斷功能,因此可以省略,這也被稱為泛型的菱形語法。

Gen<String> strGen = new Gen<String>("string type");

泛型只適用引用類型###

當聲明泛型時,傳遞的參數必須是引用類型,使用簡單類型是非法的。比如下面的聲明:

Gen<int> genInt = new Gen<int>(100);//非法的

基于不同類型參數的泛型類型是不同的###

Gen<String> strGen = new Gen<>("string type");
Gen<Integer> intGen = new Gen<>(100);

上面的兩行代碼聲明的泛型類型是不同的

strGen == intGen

上面的代碼會出現編譯錯誤。

泛型類型提升類型安全性的原理

我們先來看一個不使用泛型的例子。

public class Nor {
    Object obj;

    Nor(Object obj){
        this.obj=obj;
    }

    Object getObj(){
        return obj;
    }
}

首先編譯器不知道任何關于構造器傳入的類型信息,這是一件壞事。其二,對于數據需要進行強制類型轉換。

public class NorDemo {

    public static void main(String[] args) {
        
        Nor intNor = new Nor(11);

        int a = (Integer) intNor.getObj();

        String str = (String) intNor.getObj();
    }
}

上面的代碼可以看出,inNor.getObj()返回類型是Integer類型,但是卻賦值給了String型,但是編譯時能通過,因為編譯器不知道Object的類型,但是很遺憾,運行時肯定會有類型轉換的問題。

運行上面的代碼拋出ClassCastException異常

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at simpledemo.NorDemo.main(NorDemo.java:11)

但是使用泛型就不會發生以上情況,泛型顯示的聲明了類型參數,這讓開發人員和編譯器都知道參數的類型。

帶有兩個類型參數的泛型類

public class TwoGen <T,V>{
    T obj1;
    V obj2;

    TwoGen(T obj1, V obj2){
        this.obj1 = obj1;
        this.obj2=obj2;
    }

    T getObj1(){
        return obj1;
    }

    V getObj2(){
        return obj2;
    }
}

和一個類型參數的泛型類差不多,菱形框里的參數可以有多個,如果類型參數總是一樣,那就沒有必要聲明多個。比如下面的代碼,聲明一個類型參數就夠了。

TwoGen<String,String> t = new TwoGen<>("aaa", "bbb");

有界類型##

從前面的例子可以看出,類型參數可以是任意的,但是往往一件事沒有約束并不是好的。如果現在我們想寫一個加法器,讓任意的兩個數字相加,這些數字可以是整型、浮點型的,如果使用泛型的方式,根據前面的經驗,可以這樣編碼

public class Caculator <T> {
    T a;
    T b;

    T getSum(T a, T b){
        return a+b;//錯誤
    }
}

那么問題來了,因為T可以任意類型的,而加運算只針對與數字和字符串連接,編譯器并不知道T的類型,所以就更不知道這個+號是用來干嘛的了。

為了處理這種情況java提供了類型邊界,我們先來看使用類型邊界的實現:

public class Caculator <T extends Number> {

    double sum;

    double getSum(T a, T b){
        sum = a.doubleValue()+b.doubleValue();
        return sum;
    }

    public static void main(String[] args) {
        Caculator<Integer> integerCaculator = new Caculator<>();
        double a = integerCaculator.getSum(3,4);
        System.out.println(a);

        Caculator<Float> floatCaculator =  new Caculator<>();
        double b = floatCaculator.getSum(9.0f,10.45f);
        System.out.println(b);

        Caculator<Double> doubleCaculator = new Caculator<>();
        double c = doubleCaculator.getSum(1.0,2.4);
        System.out.println(c);
    }


}

結果:

7.0
19.449999809265137
3.4
Process finished with exit code 0

分析:

下面的代碼說明類型參數為Number的子類,也就是說將T的范圍限制在Number和Number的子類中

Caculator <T extends Number>

所以才可以使用a.doubleValue()方法,因為T繼承自Number。還有一個好處就是可以防止傳入不能處理的類型,比如String,因為String并不是Number子類,編譯會報錯。

當然也可以擴展一個或多個接口,使用&連接

Class SomeClass <T extends SuperClass & Interface1 &Interface2> { ...}

綜上,Number就是類型參數的上界,T類型只能是Number和Number的子類。

通配符參數##

上例中,我們使用了getSum()方法來求不同數字類型的兩個數之和,如果要擴展Caculator類,讓其比較兩個數之和是否相等。按照上面思路我們可以這樣做:

public class Caculator <T extends Number> {

    double sum;

    double getSum(T a, T b){
        sum = a.doubleValue()+b.doubleValue();
        return sum;
    }

    Boolean isEquals(Caculator<T> caculator){
        return this.sum == caculator.sum;
    }

    public static void main(String[] args) {
        Caculator<Integer> integerCaculator = new Caculator<>();
        double a = integerCaculator.getSum(3,4);
        System.out.println(a);

        Caculator<Double> doubleCaculator1 = new Caculator<>();
        double d = doubleCaculator.getSum(3.0,4.0);
        System.out.println(d);
        //非法,因為T的類型為Integer,isEquals()方法中傳入的類型為Caculator<String>.
        integerCaculator.isEquals(doubleCaculator1);
      
        Caculator<Integer> integerCaculator1 = new Caculator<>();
        double e = integerCaculator.getSum(1,6);
        System.out.println(e);
        //合法,因為傳入的類型參數都是Integer
        integerCaculator.isEquals(integerCaculator1);
    }


}

從上面的代碼可以看出,上面的類值能適用于參數類型相等的情況,如果我們要比較3+4和2.5+4.5,此方法就不適用了。為了實現更好的一般性,我們可以使用通配符。

先來看一下用通配符怎么實現

public class Caculator <T extends Number> {

    double sum;

    double getSum(T a, T b){
        sum = a.doubleValue()+b.doubleValue();
        return sum;
    }

    //注意,這里修改了類型參數,把T換成了?
    Boolean isEquals(Caculator<?> caculator){
        return this.sum == caculator.sum;
    }

    public static void main(String[] args) {
        Caculator<Integer> integerCaculator = new Caculator<>();
        double a = integerCaculator.getSum(11,3);
        System.out.println(a);

        Caculator<Double> doubleCaculator = new Caculator<>();
        double b = doubleCaculator.getSum(11.0, 3.0);
        System.out.println(b);
        System.out.println(doubleCaculator.isEquals(integerCaculator));

        Caculator<Integer> integerCaculator2=new Caculator<>();
        double c  = integerCaculator2.getSum(2,12);
        System.out.println(c);
        System.out.println(integerCaculator.isEquals(integerCaculator2));

    }


}

通配符----?,顧名思義就是可以給泛型類傳遞任何類型的類型參數,通配符對參數類型沒有限制,限制條件是<T extends SuperClass>決定的。

有界通配符###

下圖定義三個坐標類,FourD繼承自TreeD,ThreeD繼承自TwoD。就是說,四維坐標可以有三維坐標和二維坐標的行為,三維坐標可以有二維坐標的行為。

class TwoD {
    int x;
    int y;

    public TwoD(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class ThreeD extends TwoD {
    int z;

    public ThreeD(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }
}


class FourD extends ThreeD {
    int t;

    public FourD(int x, int y, int z, int t) {
        super(x, y, z);
        this.t = t;
    }
}

Coords類把坐標類型限制在TwoD以及TwoD的子類。

BoundeWidecard類定義了三個打印坐標的方法:

  • show2D(Coords<? extends TwoD> coords)方法可以打印TwoD、ThreeD和FourD中任意一個類的xy坐標。
  • show3D(Coords<? extends ThreeD> coords)方法可以打印ThreeD和FourD中任意一個類的xyz坐標。
  • show4D(Coords<? extends FourD> coords)方法可以打印FourD中的xyzt坐標。

class Coords<T extends TwoD> {
    T ob;

    Coords(T ob) {
        this.ob = ob;
    }
}

public class BoundeWildcard {
    static void show2D(Coords<? extends  TwoD> coords) {
        System.out.println("2d, x: " + coords.ob.x + " y: " + coords.ob.y);
    }

    static void show3D(Coords<? extends ThreeD> coords) {
        System.out.println("3d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z);
    }

    static void show4D(Coords<? extends FourD> coords) {
        System.out.println("4d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z + "z: " + coords.ob.t);
    }

    public static void main(String[] args) {
        Coords twoD = new Coords(new TwoD(11,22));
        Coords threeD = new Coords(new ThreeD(111,222,333));
        Coords fourD = new Coords(new FourD(1111,2222,3333,4444));

        show2D(twoD);
        show3D(threeD);
        show4D(fourD);

        //show3D(twoD); //運行時錯誤
        show3D(threeD);
        show3D(fourD);

        //show4D(twoD); //運行時錯誤
        //show4D(threeD); //運行時錯誤
        show4D(fourD);

    }
}

結果

2d, x: 11 y: 22
3d, x: 111 y: 222z: 333
4d, x: 1111 y: 2222z: 3333z: 4444
3d, x: 111 y: 222z: 333
3d, x: 1111 y: 2222z: 3333
4d, x: 1111 y: 2222z: 3333z: 4444

Process finished with exit code 0

泛型方法

下面我們創建一個泛型方法來判斷一個對象是都存在于一個對象數組中。

public class GenMethDemo {
    static <T extends Comparable<T>, V extends  T> boolean isIn(T x, V[] y){
        for (V v : y) {
            if ((x.compareTo(v))==0)
                return true;
        }
        return false;
    }

    public static void main(String[] args) {
        int x =2;
        Integer [] x_array = {1,2,3,4,5,6}; //不能用簡單類型創建
        Integer [] y_array = {1,3,4,5,6};
        boolean flag;

        flag = isIn(x,x_array);
        System.out.println("2 is in x_array: " + flag);

        flag = isIn(x,y_array);
        System.out.println("2 is in y_array: " +flag);


    }
}

說明參數類型T都要實現Comparable<T>泛型接口,這樣才能使用compareTo方法來比較兩個對象。V extends T表示V是T或者是T的子類,這樣聲明了兼容的數據類型,方便比較。

泛型構造函數##

可以將構造函數泛型化,即使不是泛型類。

public class GenConstructor {
    double val;

    <T extends Number> GenConstructor(T val){
        this.val = val.doubleValue();
    }

    void showVal(){
        System.out.println("val: "+ val);
    }

    public static void main(String[] args) {
        GenConstructor genConstructor = new GenConstructor(11);
        GenConstructor genConstructor1 = new GenConstructor(11.3f);

        genConstructor.showVal();
        genConstructor1.showVal();
    }
}

結果:

val: 11.0
val: 11.300000190734863

Process finished with exit code 0

泛型接口##

一般來說,泛型接口的聲明和泛型類的聲明是一樣的。值得注意的是,因為實現類需要實現泛型接口,所以實現類必須是泛型類。下面是一個泛型接口和實現的例子:

public interface GenInterface <T extends Comparable<T>> {
}

class Gen<T extends Comparable<T>> implements GenInterface<T>{

}

因為實現類實現了接口,實現類就必須和接口的參數類型相同(包含接口的參數類型),所以在implements子句中就不用再將參數類型全部寫全,而且這樣是錯誤的:

class Gen<T extends Comparable<T>> implements GenInterface<T extends Comparable<T>>{//非法
}

使用泛型接口有兩個優勢:

  • 針對不同類型實現
  • 可以給類型設置邊界

使用泛型的一些限制

不能實例化類型參數###

class Gen<T>{
    T ob;
    Gen(){
        this.ob = new T(); //非法
    }
}

很明顯,這里T只是一個占位符,系統不知道具體的類型。

對靜態成員的一些限制###

class Wrong<T>{
    static T ob; //非法,不能聲明T類型的靜態變量
    //非法,靜態方法返回值不能是T
    static T getOb(){ 
        return ob;
    }
}

盡管不能聲明某些帶有類型參數的靜態成員,但是可以聲明泛型方法。下面的泛型方法用來判斷一個對象是否存在于另外一個對象中,因為不能判斷類型的具體類型,所以使用泛型方法,可以看出泛型方法的使用范圍更加廣泛,這就是泛型的“泛”的體現。

public class GenMethDemo {
    static <T extends Comparable<T>, V extends  T> boolean isIn(T x, V[] y){
        for (V v : y) {
            if ((x.compareTo(v))==0)
                return true;
        }
        return false;
    }

    public static void main(String[] args) {
        int x =2;
        Integer [] x_array = {1,2,3,4,5,6}; //不能用簡單類型創建
        Integer [] y_array = {1,3,4,5,6};
        boolean flag;

        flag = isIn(x,x_array);
        System.out.println("2 is in x_array: " + flag);

        flag = isIn(x,y_array);
        System.out.println("2 is in y_array: " +flag);
    }
}

泛型類層次##

和非泛型類一樣,泛型類可以繼承和被繼承。但是需要注意,和非泛型類不一樣的是:泛型類需要向上傳遞超類需要類型參數,就想非泛型類中構造器向上傳遞一樣

泛型超類##

泛型超類示例:

public class Gen <T> {
    public static void main(String[] args) {
        SubGen<String> subGen = new SubGen<>();
    }
}

class SubGen<T> extends Gen<T>{

}

下面的代碼向超類傳遞了參數類型String,對于Gen來說,他的參數類型為String。

SubGen<String> subGen = new SubGen<>()

還要注意的是,泛型子類除了將類型參數傳遞給泛型超類,再也沒有使用類型參數T。所以,即使泛型超類的子類不必泛型化,其也必須指定泛型超類需要的類型參數。

當然,泛型子類也可以添加自身的類型參數

class SubGen<T,V> extends Gen<T>{

}

泛型子類###

非泛型類可以是泛型類的超類。

public class Gen  {
    
}

class SubGen<T> extends Gen{

}

強制轉換

需要注意的是,使用強制轉換時兩個泛型類的類型必須兼容,并且類型參數也要相同。

類型擦除##

通常,我們不需要知道Java源碼轉化為對象代碼的細節,但是對于泛型而言大致了解這個過程是很重要的,這有助于我們理解泛型的工作機制。

影響泛型如何添加到Java的一個最重要的約束:就是要和之前的Java版本兼容,簡單的說就是要和之前的非泛型代碼兼容,對Java語法和虛擬機做的修改不能破壞以前的代碼。為了實現泛型,Java使用了類型擦除。

總的來說,運行時沒有參數類型,所有的類型參數都會轉化為具體的類型,如果沒有界定類型,則用Object表示。

橋接方法###

有時候,編譯器需要添加一些橋接方法,比如泛型子類重寫方法的類型擦除,不能產生超類中方法的類型擦除。對于這種情況,會生成使用超類類型擦除的方法,并且這個方法調用具有由子類指定的類型擦除的方法。當然,橋接方法只會在字節碼級別發生,你不會看到,也不能使用。

比如在父類中有一個getOb()方法

public class Gen<T> {
    T ob;

    Gen(T ob){
        this.ob = ob;
    }

    T getOb(){
        return ob;
    }

}

但是這個泛型類的子類重寫了該方法

class Gen2<String> extends Gen<String>{

    Gen2(String ob) {
        super(ob);
    }

    String  getOb(){
        System.out.println("aaaa");
        return ob;
    }

}

對于上面的兩段代碼,子類Gen2擴展了Gen,但是使用特定于String的Gen版本,同時Gen2還重寫了getOb()方法。所有這些都是可以接受的,但是對于類型擦除卻稍顯麻煩。本來期待的下面的方法:

T getOb(){
    return ob;    
}

為了處理這個問題,編譯器生成一個橋接方法,這個橋接方法調用String版本的那個簽名。因此如果檢查有javap生成的Gen2類文件,就會看到下面的方法:

java.lang.String getOb();
java.lang.Object getOb(); //橋接方法

對于上面兩個方法,唯一不同的就是返回類型,通常來講這是錯誤的,因為這不是源碼引起的,JVM會正確的處理它。

對泛型數組的一些限制

不能實例化參數類型的數組,這和不能實例化參數類型的對象類似。

class Gen<T extends Number> {
    T[] a;
//    a = {1,2,3};//錯誤,不能初始化
    Gen(T[] a){
//        this.a = new T[10];//錯誤,不能實例化
        this.a=a;//可以賦值
    }
}

Java 8官方教程說指定類型參數的泛型數組不能實例化。

public class Restriction {
    public static void main(String[] args) {
        Integer[] integers = {1,2,3,4,5,6};

        Gen<Integer> gen[] = new Gen<Integer>[10]; //編譯錯誤
        Gen<?> gen1[] = new Gen<Integer>[10];//可以通過


    }
}

由于泛型內容比較多,如有遺漏請在討論區補充,本文會持續更新修改,歡迎關注。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、泛型簡介1.引入泛型的目的 了解引入泛型的動機,就先從語法糖開始了解。 語法糖 語法糖(Syntactic S...
    Android進階與總結閱讀 1,033評論 0 9
  • 1、為什么使用泛型 泛型是指參數化類型的能力。可以定義帶有泛型類型的類或類,隨后編譯器會使用具體的類型來代替它。使...
    追逐地平線的甘閱讀 449評論 0 1
  • 代碼github地址 泛型不是協變的,數組與集合類之間的區別## 雖然將集合看作是數組的抽象會有所幫助,但是數組還...
    天外之石閱讀 346評論 0 0
  • 我們知道,使用變量之前要定義,定義一個變量時必須要指明它的數據類型,什么樣的數據類型賦給什么樣的值。 假如我們現在...
    今晚打肉山閱讀 1,025評論 0 1
  • 泛型是jdk1.5里出現的一個非常重要的功能,之前一直只是會使用一點泛型,但是對泛型一些復雜的使用形式總是把握不準...
    天蝎scorpion閱讀 302評論 0 0