Java 泛型,參數類型T和通配符?的邊界問題

目錄

一、概述

二、和的使用及區別

(1)類型參數

(2)無界通配符

三、有界通配符、

四、泛型擦除


一、概述

1、定義:Java泛型(generics)泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是所操作的數據類型被指定爲一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。

2、優勢:Java語言引入泛型的優勢在於安全、重用。 泛型在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,以提高代碼的重用率。

3、作用:相比雜亂地使用Object聲明變量,再進行強制類型轉換的代碼而言,使用泛型機制編寫的程序代碼具有更好的安全性和可讀性。泛型對於集合類尤其有用,例如,List<T>容器就是一個無處不在的泛型集合類。

List<String> list=new ArrayList<>();

二、<T>和<?>的使用及區別

好奇List<T>和List<?>有什麼區別呢?其實這個問題就是問無限定類型變量“<T>”無界通配符“<?>”的區別。

討論“<T>"和"<?>",首先要區分開兩種不同的場景:

  • 第一,聲明泛型類或泛型方法。
  • 第二,使用泛型類或泛型方法。

類型參數“<T>”主要用於第一種,聲明泛型類或泛型方法; 無界通配符“<?>”主要用於第二種,使用泛型類或泛型方法。

 


(1)類型參數<T>

 

1、<T>聲明泛型類

List<T>最應該出現的地方,應該是定義一個泛型List容器。看看ArrayList的源碼:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, 
Cloneable, java.o.Serializable {
    ... ...
}

ArrayList<E>中的“E”也是類型參數。只是表示容器中元素Element的時候,習慣用“E”。

換一個簡單的例子,我們自己定義一個新泛型容器叫Box<T>。

class Box<T>{
    private T item1;
    private T item2;
}

<T>類型參數能夠對類型起到‘約束的作用,例子中就是爲了保證Box裏的item1, item2都是同一個類型T。如,Box<String>,代表兩個item都是String;Box<Integer>裏兩個item都是Integer。

*那什麼時候會使用泛型類呢?

答:就是作爲泛型類的成員字段或成員方法的參數間接出現。

class Box<T>{
    private List<T> item;//泛型類成員字段

    public List<T> get(){return item;}//方法返回值

    public void set(List<T> t){item=t;}//方法參數
}

這裏寫成List<T>爲了表示和Box<T>類型參數保持一致。


2、<T>聲明泛型方法

Function類的reduce是個靜態泛型方法,注意這方法是在普通類中定義的,而不是在泛型類中定義的。這裏的List<T>出現在reduce方法參數\函數返回值\函數內部,也是爲了保持泛型類型的一致性。

class Fuction{
    public static <T> List<T> reduce(List<T> list){
       
    }
}

注意,類型變量<T> 放在修飾符(public static)的後面,返回類型的前面。

 


(2)無界通配符<?>

 

1、聲明泛型類不能用<?>

 首先,要明確通配符<?>不能拿來聲明泛型。錯誤案例,如下:

class Box<?>{
//Error Example
    private ? item1;
    private ? item2;
}

通配符<?>作用:是使用定義好的泛型類

例如,用<?>聲明List容器的變量類型,然後用一個實例對象給它賦值的時候就比較靈活。

List<?> list = new ArrayList<String>();

2、 <?>的CAP#1問題

List<?>這個寫法非常坑。因爲,通配符<?>會捕獲具體的String類型,但編譯器不叫它String,而是起個臨時的代號,比如”CAP#1“。因此,list不能存String,任何元素都不行,只能存null。

List<?> list = new ArrayList<String>();

list.add("hello");    //ERROR
//argument mismatch; String cannot be converted to CAP#1

list.add(111);    //ERROR
//argument mismatch; int cannot be converted to CAP#1

另外,如果拿List<?>做參數,也會有奇妙的事情發生。還是剛纔Box<T>的例子,有get()和set()兩個方

class Box<T>{
    private List<T> item;
    public List<T> get(){return item;}
    public void set(List<T> t){item=t;}

    //把item取出來,再放回去, ERROR
    public void getSet(Box<?> box){box.set(box.get());
    //error: incompatible types: Object cannot be converted to CAP#1
}    
}

新的getSet()方法,只是把item先用get()方法讀出來,然後再用set()方法存回去。按理說不可能有問題。實際運行卻會報錯。原因和前面一樣,用通配符<?>聲明的類型泛型類Box,調用box.set()的參數類型被編譯器捕獲,命名爲'CAP#1',和box<?>.get()返回的Object對象無法匹配。

3、解決方法

通過寫一個helper()輔助函數。

class Box<T>{
    private List<T> item;
    public List<T> get(){return item;}
    public void set(List<T> t){item=t;}

    //getSet()方法存取元素
    public void getSet(Box<?> box){helper(box);}
    //helper()輔助函數
    public <V> void helper(Box<V> box){box.set(box.get());}
}

三、有界通配符<? extends X>、<? super X>

常用的是<? extends X>或者<? super XXX>兩種,帶有上下界的通配符。

其中,上界可以有多層<? extends X extends XX>。

 

注意:

  • 對於無界通配符<?> extends 的通配符限定泛型,我們無法向裏面添加元素(只可以添加null),只能讀取其中的元素。
  • 對於無界通配符<?> super 的通配符限定泛型,我們可以讀取其中的元素,但讀取出來的元素會變爲 Object 類型。

 

 


四、泛型擦除

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

我們先看一個例子:

 ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

System.out.println(list1.getClass() == list2.getClass());

 打印結果:true

結果顯示list1和list2居然是同一個類型的ArrayList,在運行時我們傳入的類型變量String和Integer都被擦除掉了,只剩下原始類型。原因是Java語言泛型在設計的時候爲了兼容原來的舊代碼,虛擬機並不知道泛型的存在,因爲泛型在編譯階段就已經被處理成普通的類和方法了。類型擦除規則:

  • 若泛型類型沒有指定具體類型,用Object作爲原始類型;

  • 若有限定類型< T exnteds XClass >,使用XClass作爲原始類型;

  • 若有多個限定< T exnteds XClass1 & XClass2 >,使用第一個邊界類型XClass1作爲原始類型;

 

 

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