夯實Java基礎(十八)——泛型

一、什麼是泛型

泛型是Java1.5中出現的新特性,也是最重要的一個特性。泛型的本質是參數化類型,也就是說所操做的數據類型被指定爲一個參數。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口、泛型方法。這個類型參數將在程序運行時肯定。java

咱們能夠把泛型理解爲做用在類或者接口上面的標籤。根據這個標籤的類型傳入規定的數據類型,不然就會出錯,其中類型必須是類類型,不能是基本數據類型。例如咱們國家中醫存藥的箱子,每一個箱子上面都貼有一個標籤,若是上面貼的是冬蟲夏草,那麼就只能放冬蟲夏草,而和其餘的藥物混合放在一塊兒就很是的亂,很容易出現錯誤。安全

泛型就是這樣的道理,咱們先來看下泛型最簡單的使用吧:ide

ArrayList<String> list=new ArrayList<>();
list.add("Hello");
//只能放字符串,若是放數字編譯報錯
//list.add(666);

注意:Java1.7以後泛型能夠簡化,就是變量前面的參數類型必需要寫,然後面的參數類型能夠寫出來,也能夠省略不寫。性能

二、爲何要泛型

簡單舉個例子,這個應該是網上最經典的例子:學習

    //建立集合對象
    ArrayList list=new ArrayList();
    list.add("Hello");
    list.add("World");
    list.add(111);
    list.add('a');
    //遍歷集合內容
    for (int i = 0; i < list.size(); i++) {
        String str= (String) list.get(i);
        System.out.println(str);
    }

上面程序運行結果毫無疑問會出現異常java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String,這就是沒有泛型的弊端。由於上面的ArrayList中就能夠存聽任意類型(即Object類型),咱們知道,全部的類型均可以用Object類型來表示,而Object在類型轉換方面很容易出現錯誤。測試

也許有人會想能夠先用Object類型代替String接收,而後再轉成相對應的數據類型,這樣是也能夠的,可是萬一在轉換的時候某個數據類型看錯了或者記錯了,那麼還不是會出現轉換異常。就算運氣好所有都對了,可是這種強制類型轉換一大堆的代碼,你的同事或者領導看了可能會拿刀來砍死你,很是不利於代碼後期的維護。this

可見沒有泛型是萬萬不能的,同時咱們還能得出泛型帶來的一些好處:spa

①、能夠有效的防止類型轉換異常的出現。.net

②、可讓代碼更簡潔,從而提升代碼的可讀性、可維護性和穩定性。3d

③、能夠加強for循環遍歷集合,進而提高性能,由於都是相同類型的。

④、能夠解決類型安全編譯警告。由於沒有泛型時全部類型向上轉型爲Object類型,因此存在類型安全問題。

泛型它有三種使用方式,分別爲:泛型類、泛型接口、泛型方法。咱們接下來學習它們怎麼使用。

三、泛型類

泛型類就是把泛型定義在類上,它的使用比較的簡單。咱們先來定義一個最普通的泛型類:

//定義泛型類,其中T表示一個泛型標識
public class Generic<T> {
 
    private T key;

    public Generic() {
    }
   
    public Generic(T key) {
        this.key = key;
    }
    //這裏不是泛型方法,它們是有區別的
    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

泛型類的測試代碼以下,咱們只需在類實例化的時候傳入想要的類型,而後泛型類中的T就會自動轉換成該相應的類型。

    public static void main(String[] args) {
        //有參構造器初始化,傳入String類型
        Generic<String> generic = new Generic<>("Generic1...");
        System.out.println(generic.getKey());//Generic1...
        //無參構造器初始化,傳入Integer類型
        Generic<Integer> generic1 = new Generic<>();
        generic1.setKey(123456);
        int key = generic1.getKey();
        System.out.println(key);//123456
    }

而後咱們再來看下泛型中泛型標識符,在上面這個泛型類中 T 表示的是任意類型,泛型中還有不少這樣的標識,例如K表示鍵,V表示值,E表示集合,N表示數子類型等等。

在泛型類中還有兩種特殊的使用方式,是關於繼承的時候是否傳入具體的參數。

①、當繼承泛型類的子類明確傳入泛型實參時:

//子類中明確傳入泛型參數類型
class SubGeneric extends Generic<String>{

}
//測試類
class Test{
    public static void main(String[] args) {
        SubGeneric subGeneric = new SubGeneric();
        //調用繼承自父類的屬性
        subGeneric.setKey("SubGeneric...");
        String key = subGeneric.getKey();
        System.out.println(key);
    }
}

能夠得出子類在繼承了帶泛型的父類時,明確的指明瞭傳入的參數類型,那麼子類在實例化時,不須要再指明泛型,用的是父類的類型。

②、當繼承泛型類的子類沒有明確傳入泛型實參時:

//子類沒有明確傳入泛型參數類型
class SubGeneric<T> extends Generic<T>{
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

class Test{
    public static void main(String[] args) {
        SubGeneric<Integer> subGeneric = new SubGeneric<>();
        //調用繼承自父類的屬性
        subGeneric.setKey(123456);
        int key = subGeneric.getKey();
        System.out.println(key);
        //調用子類本身的屬性
        SubGeneric<String> subGeneric1=new SubGeneric<>();
        subGeneric1.setValue("SubGeneric...");
        String value = subGeneric1.getValue();
        System.out.println(value);
    }
}

當子類沒有明確指明傳入的參數類型時,那麼在子類實例化時,都是根據子類中傳入的類型來肯定的父類的類型。若是父類和子類中有一個明確,另外一個沒有明確或者二者明確的類型不同,那麼它們的類型會自動提高爲Object類型。以下:

//一個明確,一個不明確
class SubGeneric<T> extends Generic<String>{
    private T value;
}

四、泛型接口

泛型接口和泛型類的定義和使用幾乎相同,只是在語句上面有些不一樣罷了,因此這裏就很少說什麼了。直接來看一下例子:

//定義一個泛型接口
public interface IGeneric<T> {
    public T show();
}

class Test implements IGeneric<String>{
    
    @Override
    public String show() {
        return "hello";
    }
}

泛型接口實現類中是否明確傳入參數和泛型類是同樣的,因此就不說了,能夠參考泛型類。

五、泛型方法

前面介紹了泛型類和泛型接口,它們二者的使用相對來講比較的簡單,而後咱們再來看一下泛型方法,泛型方法比它們二者稍微複雜一點點。在前面的泛型類中咱們也看到了方法中有用到泛型,它的格式是這樣的:

    //這裏不是泛型方法,它們是有區別的
    public T getKey() {
        return key;
    }

可是它並非泛型方法。泛型方法的中的返回值必須是用 <泛型標識> 來修飾(包括void),只有聲明瞭<T>的方法纔是泛型方法,這裏<T>代表該方法將使用泛型標識T,此時才能夠在方法中使用泛型標識T。而單獨只用一個泛型標識 T 來表示會報錯,系統沒法解析它是什麼。固然咱們也可使用其餘的泛型標識符如:K、V、E、N等。

泛型方法的聲明格式以下:

    //這裏的泛型標識 T 與泛型類中的 T 沒有任何關係 
    public <T> T show(T t){
        return t;
    }

注意:泛型方法的泛型與所屬的類的泛型沒有任何關係。咱們能夠舉例說明:

public class GenericMethod<T> {

    //這裏的泛型標識 T 與泛型類中的 T 沒有任何關係
    public <T> T show(T t){
        return t;
    }
}

class Test{
    public static void main(String[] args) {
        //實例化泛型類,傳入String類型
        GenericMethod<String> genericMethod = new GenericMethod<>();
        //調用方法,傳入數字
        Integer show = genericMethod.show(123456);
        System.out.println(show);//123456
        //調用方法,傳入字符
        Character a = genericMethod.show('a');
        System.out.println(a);//a
    }
}

在測試類中,建立類的實例時,泛型傳入的是String類型,而在調用方法的時候,分別傳入了數字類型和字符類型。

至此咱們得出結論:泛型方法是在調用方法的時候指明泛型的具體類型,因此泛型方法能夠是靜態的;泛型類和泛型接口是在實例化類的時候指明泛型的具體類型,因此泛型類和泛型接口中的方法不能是靜態的。

六、泛型在繼承方面的體現(兼容性)

若是某個類繼承了另外一個類,那麼它們之間的轉換就會變得簡單,例如Integer繼承Number:

    Number number=123;
    Integer integer=456;
    number=integer;//能夠賦值

    Number[] numbers=null;
    Integer[] integers=null;
    numbers=integers;//能夠賦值

上面這麼作徹底能夠,可是把它們用在泛型中卻不是這麼一回事了。

    List<Number> list1=null;
    List<Integer> list2=null;
    //編譯報錯,不兼容的類型
    //list1=list2;

這是由於Integer繼承自Number類,可是List<Integer>並非繼承自List<Number>的,它們兩是都繼承自Object這個根父類。看到下面這張圖片可能會更好的理解(圖片引用自https://blog.csdn.net/whdalive/article/details/81751200)

 

這裏用這樣一段話來歸納:雖然類A是類B的父類,可是G<A>和G<B>它們之間不具有任何子父類關係,兩者都是並列關係,惟一的關係就是都繼承自Object這個根父類。

再來看一下另外一種狀況:帶泛型的類(接口)與另外一個類(接口)有繼承(實現)的關係。

舉例:在集合中,ArrayList <E>實現List <E> , List <E>擴展Collection <E> 。 所以ArrayList <String>是List <String>的子類型,List <String>是Collection <String>的子類型。 因此只要不改變類型參數,就會在類型之間保留子類型關係。

    Collection<String> collection=null;
    List<String> list=null;
    ArrayList<String> arrayList=null;
    collection=list;
    list=arrayList;

這裏其實就是普通的繼承(實現),相似於Integer繼承在Number類同樣。圖片以下:

參考文章:https://blog.csdn.net/whdalive/article/details/81751200

七、通配符

爲何要用通配符呢?那確定是泛型在某些地方還不是很是完美,因此纔要用到通配符呀,咱們來分析一下:

在上面的一節中咱們講了Integer是Number的一個子類,而List<Integer>和List<Number>兩者都是並列關係,它們之間毫無關係,惟一的關係就是都繼承自Object這個根父類。那麼問題來了,咱們在使用List<Number>做爲方法的形參時,可否傳入List<Integer>類型的參數做爲實參呢?其實這個時候答案已經很明顯了,是不能的。因此這時候就能夠用到通配符了。

在沒有使用通配符的狀況下咱們的代碼要這樣寫:

public class Generic {

    public static void main(String[] args) {
        List<Number> list1=new ArrayList<>();
        list1.add(1);
        list1.add(2);
        List<Integer> list2=new ArrayList<>();
        list2.add(3);
        list2.add(4);
        show(list1);
        //報錯說不能應用Integer類型
        //show(list2);
    }

    public static void show(List<Number> list){
        System.out.println(list.toString());//[1, 2]
    }
}

若是還想要支持Integer類型則須要添加新的方法:

    public static void show1(List<Integer> list){
        System.out.println(list.toString());//[1, 2]
    }

這樣就會致使代碼大量的冗餘,很是不利於閱讀。因此此時就須要泛型的通配符了,格式以下:

    public static void show(List<?> list){
        System.out.println(list.toString());//[1, 2][3, 4]
    }

當咱們使用了通配符以後,就能輕鬆解決以上問題了。在Java泛型中用  號用來表示通配符,?號通配符表示當前能夠匹配任意類型,任意的Java類均可以匹配。可是在使用通配符以後必定要注意:該對象中的有些方法任然能夠調用,而有些方法則不能調用了。例如:

    public static void show(List<?> list){
        //list.add(66);//不能使用add()
        list.add(null);//惟獨能添加的只有null
        Object remove = list.remove(0);//可使用remove()
        System.out.println(remove);
        Object get = list.get(0);//可使用get()
        System.out.println(get);
        System.out.println(list.toString());
    }

這是由於只有調用的時候才知道List<?>通配符中的具體類型是什麼。因此對於上面的list而言,若是調用add()方法,那麼咱們並不知道要添加什麼樣的類型,因此會報錯。而remove()和get()方法都是根據索引來操做的,它們都是數字類型,因此它是能夠被調用的。

通配符還有兩種擴展的用法:

7.1通配符上限

List<? extends ClassName>,它表示的是傳入的參數必須是ClassName的子類(包括該父類),相似於數學中(∞,ClassName],而後簡單舉例說明:

    public static void main(String[] args) {
        List<Number> list1=new ArrayList<>();
        list1.add(1);
        List<Integer> list2=new ArrayList<>();
        list2.add(2);
        List<Object> list3=new ArrayList<>();
        list3.add(3);
        show(list1);//傳入Number類型
        show(list2);//傳入Integer類型
        //show(list3);//傳入Object類型,報錯說不能應用Object類型,由於這裏最大隻能用到Number類型
    }

    public static void show(List<? extends Number> list){
        System.out.println(list.toString());
    }

7.2通配符下限

List<? super ClassName>,它表示的是傳入的參數必須是ClassName的父類(包括該子類),相似於數學中[ClassName,∞)

    public static void main(String[] args) {
        List<Number> list1=new ArrayList<>();
        list1.add(1);
        List<Integer> list2=new ArrayList<>();
        list2.add(2);
        List<Object> list3=new ArrayList<>();
        list3.add(3);
        show(list1);//傳入Number類型
        //show(list2);//傳入Integer類型,報錯說不能應用Integer類型,由於此時最小隻能用到Number類型
        show(list3);//傳入Object類型,
    }

    public static void show(List<? super Number> list){
        System.out.println(list.toString());
    }
相關文章
相關標籤/搜索