泛型怎麼玩

泛型怎麼玩


1.什麼是泛型

參數化類型,把類型當做參數傳遞給類或者方法,創建對象或者調用方法才明確類型;

Java泛型的設計原則:只要在編譯期沒有錯誤或警告,在運行期就不回出現ClassCastException異常。

2.泛型的好處

如集合類,如果沒有泛型之前,我們不清楚集合類的元素類型,很有可能會發生ClassCastException異常或者需要類型裝置轉換,如:

List list = new ArrayList();
list.add("123");
list.add(new Integer(123));
Object obj = list.get(0);

List對內部元素是沒有任何限制的,裝載String或者Integer都是沒有任何語法錯誤的,只知道是Object類型,當get的時候只能返回Object,如要返回String則需要進行類型向下強制轉換,這樣程序就不安全,使用不當就會發生異常。

支持過泛型類之後,可以把類型傳給List(指定List內部元素類型),在編譯期就確定了List的元素類型,使得程序可以更安全、健壯,如:

List<String> list = new ArrayList<String>();
list.add("123");
list.add(new Integer(123));//編譯期會發出錯誤警告
String str = list.get(0);

有了泛型後,get獲取元素可以直接返回String,避免類型的強制轉換,提高了:

  • 代碼可讀性 (可以更清晰的讀出包裝類型限制)
  • 代碼的健壯性[避免了集合類型的強制轉換]
  • 代碼更簡潔 (可以更優雅的實現需要動態設定參數類型功能等)

3.如何使用泛型

但出於規範的目的,Java 還是建議我們用單個大寫字母來代表類型參數。常見的如:

泛型類

泛型類就是把泛型定義在類上,在使用此類的時候需要關聯泛型參數時才明確下來,這樣的話可以動態的使用一類抽象類(需要使用才明確什麼類型),不需要擔心轉換問題。
定義在類上,該類的所有方法、屬性都可以使用這一類型參數

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }

    public static void main(String[] args) {
        Test<String> t = new Test<String>("123");
        String a = t.obj;

        Test<Integer> t1 = new Test<Integer>(123);
        Integer b = t1.obj;
    }
}

用戶想要使用哪種類型,就在創建的時候指定類型。使用的時候,該類就會自動轉換成用戶想要使用的類型了。

**上例代碼的T泛型參數,是JAVA的常用寫法,你也可以換成任意的字符;但出於規範的目的,Java還是建議我們用單個大寫字母來代表類型參數。**常見的如:

  • T 代表一般的任何類。
  • E 代表 Element 的意思,或者 Exception 異常的意思。
  • K 代表 Key 的意思。
  • V 代表 Value 的意思,通常與 K 一起配合使用。
  • S 代表 Subtype 的意思。
泛型類派生出的子類

上述泛型類本質上還是一個JAVA類,只不過多了泛型類型這個參數,所以泛型類也是可以繼承的。
但子類可以明確泛型類型參數變量,也可以不明確:
子類明確泛型類型參數

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }
}
/**
* 子類明確泛型參數類型
*/
class TestA extends Test<String>{
    public TestA(String Obj) {
        super(Obj);
    }
}

子類不明確泛型類型參數

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }
}
/**
* 子類不明確泛型參數類型
*/
class TestA<T> extends Test<T>{
    public TestA(T Obj) {
        super(Obj);
    }
}

當子類不明確泛型類的類型參數變量時,外界使用子類的時候,也需要傳遞類型參數變量進來,在實現類上需要定義出類型參數變量

泛型方法

在某一個方法上使用泛型,有時外界只關心某個方法,而不關心整個類,就可以使用泛型方法
定義泛型方法:

public class Test {
    public <T> void print(T t){
        System.out.println(t);
    }

    public static void main(String[] args) {
        Test t = new Test();
        //用方法,傳入的參數是什麼類型,返回值就是什麼類型
        t.print("123");

        t.print(123);
    }
}

泛型通配符

類型通配符一般是使用?代替具體的類型參數,例如 List<?> 在邏輯上是List,List 等所有List<具體類型實參>的父類。

public class Test{

    public void test(List<?> list){
        //只讀,不能使用add一類的操作
        list.get(1);
    }

    public static void main(String[] args) {
        Test t = new Test();
        //用方法,傳入的參數是什麼類型,返回值就是什麼類型
        t.test(new ArrayList<String>());

        t.test(new ArrayList<Integer>());
    }
}

注意:?泛型對象是隻讀的,不可修改,因爲?類型是不確定的,可以代表範圍內任意類型;

類型通配符上限
可以設定?類型通配符可接受類型的上限,只能裝載上限的子類或自身

? extends Number

類型通配符下限
顧名思義,可以設定?類型通配符可接受類型的下限,只能裝載下限的父類或自身

//傳遞進來的只能是Type或Type的父類
? super Number

通配符和泛型方法

所有能用類型通配符(?)解決的問題都能用泛型方法解決,並且泛型方法可以解決的更好:

a. 類型通配符:void func(List<? extends A> list);
b. 完全可以用泛型方法解決:<T extends A> void func(List<T> list);

上面兩種方法可以達到相同的效果(?可以代表範圍內任意類型,而T也可以傳入範圍內的任意類型實參),並且泛型方法更進一步,?泛型對象是隻讀的,而泛型方法裏的泛型對象是可修改的,即List list中的list是可修改的!
兩者區別:

通配符可以使用超類(super)限定而類型參數不行
?和 T 都表示不確定的類型,區別在於我們可以對 T 進行操作,但是對 ?不行
類型參數可以多重限定而通配符不行(T extends A,B)

?相對來說更靈活,代碼更簡潔,二者各有各的應用場景:

  • 一般只讀就用?,要修改就用泛型方法
  • 在多個參數、返回值之間存在類型依賴關係就應該使用泛型方法,否則就應該是通配符?

4.類型擦除

Java的類庫是Java生態中非常寶貴的財富,必須保證向後兼容(即現有的代碼和類文件依舊合法)和遷移兼容(泛化的代碼和非泛化的代碼可互相調用)基於上面這兩個背景和考慮,Java設計者採取了“類型擦除”這種折中的實現方式。

Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成爲類型擦除。

原始類型

類型擦除之後爲什麼,我們需要進行類型轉換呢?原因是類型擦除之後,最後在字節碼中的類型變量會轉譯成原始類型。

在無限定泛型類型,會被轉譯成Object的原始類型。
有限定泛型類型,會被其限定類型替換

帶來的問題

多態的衝突
子類重寫父類方法,如父類方法使用了泛型,由於類型擦除,父類泛型參數都會轉譯成Object類型,子類重寫方法明確泛型類型,就會導致參數類型不一致,重載@Override不成立。
如何處理呢?
橋方法:在子類變異後,會生成橋方法來實現重👟父親
反射調用問題
既然泛型類型在編譯期都會被擦除,轉移成上限(Object),那通過反射的方式調用機會導致,類型不匹配胃問題有可能導致ClassCastException異常:

public class Test{
    public <T> void test(List<T> list,T e){
        //只讀,不能使用add一類的操作
        list.add(e);
    }
    public <T> void print(List<T> list){
        if(list!=null){
            for(T t: list){
                System.out.println(t);
            }
        }
    }
    public static void main(String[] args) throws Exception{
        List<String> list = new ArrayList<String>();
        Test t = new Test();
        t.test(list,"123");
        //一定寫object.class 表示什麼類型都可以加
        Method method = Test.class.getMethod("test", List.class,Object.class);
        method.invoke(t, list,new Integer(456));
        t.print(list);
    }
}

上例,list限定泛型類型爲String,但是class字節碼把String參數類型擦除,通過反射調用,可以傳入Object對象,從而把Integer對象加入List。

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