都2020了,還很差好學學泛型?

1、概述

Java 泛型(generics)是 JDK 1.5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制容許開發者在編譯時檢測到非法的類型。java

1.1 什麼是泛型?

  • 泛型,即參數化類型

一提到參數,最熟悉的就是定義方法時有形參,而後調用此方法時傳遞實參。那麼參數化類型怎麼理解呢?顧名思義,就是將類型由原來的具體的類型參數化,相似於方法中的變量參數,此時類型也定義成參數形式(能夠稱之爲類型形參),而後在使用/調用時傳入具體的類型(類型實參)。git

  • 泛型的本質是爲了參數化類型

在不建立新的類型的狀況下,經過泛型指定的不一樣類型來控制形參具體限制的類型。也就是說在泛型使用過程當中,操做的數據類型被指定爲一個參數,這種參數類型能夠用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。github

1.2 舉個栗子:

@Test
public void genericDemo() {
    List list = new ArrayList();
    list.add("風塵博客");
    list.add(100);

    for(int i = 0; i< list.size();i++){
        String item = (String)list.get(i);
        log.info("item:{}", item);
    }
}
複製代碼

毫無疑問,程序的運行結果會以崩潰結束:數組

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
複製代碼

ArrayList能夠存聽任意類型,例子中先添加了一個String類型,又添加了一個Integer類型。使用時都以String的方式使用,所以程序崩潰了。爲了解決相似這樣的問題(在編譯階段就能夠解決),泛型應運而生。安全

1.3 特性

泛型只在編譯階段有效app

  1. 在編譯的時候可以檢查類型安全,而且全部的強制轉換都是自動和隱式的;
  2. 在邏輯上看以當作是多個不一樣的類型,實際上都是相同的基本類型。

2、泛型的使用

泛型有三種使用方式,分別爲:泛型類、泛型接口、泛型方法。dom

2.1 泛型類

泛型類型用於類的定義中,被稱爲泛型類。經過泛型能夠完成對一組類的操做對外開放相同的接口。最典型的就是各類容器類,如:ListSetMapjvm

2.1.1 一個最普通的泛型類:

public class Generic<T> {
    /** * key這個成員變量的類型爲T,T的類型由外部指定 */
    private T key;

    /** * 泛型構造方法形參key的類型也爲T,T的類型由外部指定 * @param key */
    public Generic(T key) {
        this.key = key;
    }

    /** * 泛型方法getKey()的返回值類型爲T,T的類型由外部指定 * @return */
    public T getKey(){
        return key;
    }
}
複製代碼

說明:ide

  1. 此處T能夠隨便寫爲任意標識,常見的如TEKV等形式的參數經常使用於表示泛型;
  2. 在實例化泛型類時,必須指定T的具體類型。

2.1.2 泛型的使用

  • 指定泛型類型
@Test
public void genericDemoWithType() {
    //泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型,好比這裏Integer改成int編譯將不經過
    Generic<Integer> integerGeneric = new Generic<Integer>(123456);
    log.info("integerGeneric key is:{}", integerGeneric.getKey());

    //傳入的實參類型需與泛型的類型參數類型相同,即爲String.
    Generic<String> stringGeneric = new Generic<String>("風塵博客");
    log.info("stringGeneric key is:{}", stringGeneric.getKey());
}
複製代碼
  • 不指定泛型類型

若是不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型能夠爲任何的類型。post

@Test
public void genericDemoWithOutType() {
    Generic generic = new Generic("111111");
    Generic generic1 = new Generic(4444);
    Generic generic2 = new Generic(55.55);
    Generic generic3 = new Generic(false);
    log.info("generic key is:{}",generic.getKey());
    log.info("generic1 key is:{}",generic1.getKey());
    log.info("generic2 key is:{}",generic2.getKey());
    log.info("generic3 key is:{}",generic3.getKey());
}
複製代碼

打印結果

... generic key is:111111
... generic1 key is:4444
... generic2 key is:55.55
... generic3 key is:false
複製代碼

2.1.3 泛型類小結

  1. 泛型的類型參數只能是類類型,不能是簡單類型;
  2. 不能對確切的泛型類型使用instanceof操做。

2.2 泛型接口

泛型接口與泛型類的定義及使用基本相同。泛型接口常被用在各類類的生產器中,例如:

public interface Generator<T> {
    public T next();
}
複製代碼
  • 當實現泛型接口的類,未傳入泛型實參時

未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一塊兒加到類中。

public class FruitGenerator<T> implements Generator<T>{

    public T next() {
        return null;
    }
}
複製代碼
  • 當實現泛型接口的類,傳入泛型實參

在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則全部使用泛型的地方都要替換成傳入的實參類型。

public class VegetablesGenerator implements Generator<String>{

    private String[] vegetables = new String[]{"Potato", "Tomato"};

    public String next() {
        Random rand = new Random();
        return vegetables[rand.nextInt(2)];
    }
}
複製代碼

本小節示例代碼地址

2.3 泛型方法

java中,泛型類的定義很是簡單,可是泛型方法就比較複雜了。

咱們見到的大多數泛型類中的成員方法也都使用了泛型,有的甚至泛型類中也包含着泛型方法。

  • 泛型類和泛型方法的區別
名稱 泛型類 泛型方法
區別 是在實例化類的時候指明泛型的具體類型 是在調用方法的時候指明泛型的具體類型

2.3.1 定義

public <T> T showKeyName(GenericMethodDemo<T> container){    
    return null;
}
複製代碼
  1. 首先在public與返回值之間的<T>必不可少,這代表這是一個泛型方法,而且聲明瞭一個泛型T
  2. 這個T能夠出如今這個泛型方法的任意位置;
  3. 泛型的數量也能夠爲任意多個。
public class GenericMethodDemo {

    /** * 泛型類 * @param <T> */
    public class Generic<T> {
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        /** * 這裏雖然在方法中使用了泛型,可是這並非一個泛型方法, * 這只是類中一個普通的成員方法,只不過他的返回值是在聲明泛型類已經聲明過的泛型, * 因此在這個方法中才能夠繼續使用 T 這個泛型。 * @return */
        public T getKey() {
            return key;
        }

        /** * 這纔是一個真正的泛型方法 * @param container * @param <T> * @return */
        public <T> T keyName(Generic<T> container){
            T test = container.getKey();
            return test;        }

        /** * 這也不是一個泛型方法,這就是一個普通的方法,只是使用了Generic<Number>這個泛型類作形參而已。 * @param obj */
        public void showKeyValue1(Generic<Number> obj){

        }

        /** * 這也不是一個泛型方法,這也是一個普通的方法,只不過使用了泛型通配符? * @param obj */
        public void showKeyValue2(Generic<?> obj){

        }


        /** * 該方法編譯器會報錯 * 雖然咱們聲明瞭<T>,也代表了這是一個能夠處理泛型的類型的泛型方法。 * 可是隻聲明瞭泛型類型T,並未聲明泛型類型E,所以編譯器並不知道該如何處理E這個類型。 * @param container * @param <T> * @return */
        public <T> T showKeyName(Generic<E> container){
            return null;
        }
        
    }
}
複製代碼

詳見 Githu GenericMethodDemo.java

2.3.2 泛型方法的使用

泛型方法能夠出現雜任何地方和任何場景中使用,可是有一種狀況是很是特殊的,泛型方法出如今泛型類中

public class GenericFruit {

    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{

        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型類中聲明瞭一個泛型方法,使用泛型E,這種泛型E能夠爲任意類型。能夠類型與T相同,也能夠不一樣。
        //因爲泛型方法在聲明的時候會聲明泛型<E>,所以即便在泛型類中並未聲明泛型,編譯器也可以正確識別泛型方法中識別的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型類中聲明瞭一個泛型方法,使用泛型T,注意這個T是一種全新的類型,能夠與泛型類中聲明的T不是同一種類型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    @Test
    public void test() {

        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子類,因此這裏能夠
        generateTest.show_1(apple);
        //編譯器會報錯,由於泛型類型實參指定的是Fruit,而傳入的實參類是Person
        //generateTest.show_1(person);

        //使用這兩個方法均可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用這兩個方法也均可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}
複製代碼

詳見 Githu GenericFruitTest.java

2.3.3 靜態方法與泛型

靜態方法沒法訪問類上定義的泛型;若是靜態方法操做的引用數據類型不肯定的時候,必需要將泛型定義在方法上。

若是寫成以下,編譯器會報錯

public staticvoid show(T t){
    
}
複製代碼
  • 正確寫法:
public static <T> void show(T t){
    
}
複製代碼

2.3.4 泛型方法小結

泛型方法能使方法獨立於類而產生變化,如下是一個基本的指導原則:

不管什麼時候,若是你能作到,你就該儘可能使用泛型方法。也就是說,若是使用泛型方法將整個類泛型化,那麼就應該使用泛型方法。另外對於一個static的方法而已,沒法訪問泛型類型的參數。因此若是static方法要使用泛型能力,就必須使其成爲泛型方法。

3、泛型通配符

咱們在定義泛型類,泛型方法,泛型接口的時候常常會遇見不少不一樣的通配符,好比 TEKV 等等,這些通配符又都是什麼意思呢?

3.1 經常使用的 TEKV

本質上這些都是通配符,沒啥區別,只不過是編碼時的一種約定俗成的東西。好比上述代碼中的 T ,咱們能夠換成 A-Z 之間的任何一個字母均可以,並不會影響程序的正常運行,可是若是換成其餘的字母代替 T ,在可讀性上可能會弱一些。一般狀況下,TEKV 是這樣約定的:

  1. :表示不肯定的 java 類型;
  2. T (type):表示具體的一個java類型;
  3. K V (key value):分別表明java鍵值中的Key/Value
  4. E (element):表明Element

3.2 ?無界通配符

對於不肯定或者不關心實際要操做的類型,可使用無限制通配符(尖括號裏一個問號,即 <?> ),表示能夠持有任何類型。

3.3 上界通配符 <? extends E>

上界:用 extends 關鍵字聲明,表示參數化的類型多是所指定的類型,或者是此類型的子類。

public void showKeyValue(Generic<? extends Number> obj){
    log.info("value is {}", obj.getKey());
}

@Test
public void testForUp() {
    Generic<String> generic1 = new Generic<String>("11111");
    Generic<Integer> generic2 = new Generic<Integer>(2222);
    Generic<Float> generic3 = new Generic<Float>(2.4f);
    Generic<Double> generic4 = new Generic<Double>(2.56);

    /*// 這一行代碼編譯器會提示錯誤,由於String類型並非Number類型的子類 showKeyValue(generic1);*/

    showKeyValue(generic2);
    showKeyValue(generic3);
    showKeyValue(generic4);
}
複製代碼

在類型參數中使用 extends 表示這個泛型中的參數必須是 E 或者 E 的子類,這樣有兩個好處:

  1. 若是傳入的類型不是 E 或者 E 的子類,編譯不成功;
  2. 泛型中可使用 E 的方法,要否則還得強轉成 E 才能使用。

3.4 下界通配符 < ? super E>

下界: 用 super 進行聲明,表示參數化的類型多是所指定的類型,或者是此類型的父類型,直至 Object

在類型參數中使用 super 表示這個泛型中的參數必須是 E 或者 E 的父類。

泛型的上下邊界添加,必須與泛型的聲明在一塊兒

實例代碼地址

3.5 ?T 的區別

?T 都表示不肯定的類型,區別在於咱們能夠對 T 進行操做,可是對 ? 不行,好比以下這種 :

// 能夠
T t = operate();

// 不能夠
? car = operate();
複製代碼

即:T 是一個肯定的類型,一般用於泛型類和泛型方法的定義,?是一個不肯定的類型,一般用於泛型方法的調用代碼和形參,不能用於定義類和泛型方法。

3.6 Class<T>Class<?> 區別

Class<T> 在實例化的時候,T 要替換成具體類。Class<?> 它是個通配泛型,?能夠表明任何類型,因此主要用於聲明時的限制狀況。好比,咱們能夠這樣作申明:

// 能夠
public Class<?> clazz;

// 不能夠,由於 T 須要指定類型
public Class<T> clazzT;
複製代碼

因此當不知道定聲明什麼類型的 Class 的時候能夠定義一 個Class<?>。 那若是也想 public Class<T> clazzT; 這樣的話,就必須讓當前的類也指定 T

public class Wildcard<T> {

    public Class<?> clazz;

    public Class<T> clazzT;
}
複製代碼

4、泛型中值得注意的地方

4.1 類型擦除

泛型信息只存在於代碼編譯階段,在進入 JVM 以前,與泛型相關的信息會被擦除掉,專業術語叫作類型擦除。

public class GenericTypeErase {

    public static void main(String[] args) {
        List<String> l1 = new ArrayList<String>();
        List<Integer> l2 = new ArrayList<Integer>();
        System.out.println(l1.getClass() == l2.getClass());

    }
}
複製代碼

打印的結果爲 true;是由於 List<String>List<Integer>jvm 中的 Class 都是 List.class,泛型信息被擦除了。

4.2 泛型類或者泛型方法中,不接受 8 種基本數據類型

須要使用它們對應的包裝類。

4.3 Java 不能建立具體類型的泛型數組

List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
複製代碼

List<Integer>List<Boolean>jvm 中等同於List<Object>,全部的類型信息都被擦除,程序也沒法分辨一個數組中的元素類型具體是 List<Integer>類型仍是 List<Boolean>類型。

4.4 強烈建議你們使用泛型

它抽離了數據類型與代碼邏輯,本意是提升程序代碼的簡潔性和可讀性,並提供可能的編譯時類型轉換安全檢測功能。

5、總結

5.1 示例源碼

Githu 示例代碼

5.2 參考文章

  1. java 泛型詳解-絕對是對泛型方法講解最詳細的,沒有之一
  2. 聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

5.3 技術交流

Github 示例代碼

  1. 風塵博客:https://www.dustyblog.cn
  2. 風塵博客-掘金
  3. 風塵博客-博客園
  4. Github
  5. 公衆號
    風塵博客
相關文章
相關標籤/搜索