都2020了,還不好好學學泛型?


一、概述

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

1.1 什么是泛型?

  • 泛型,即參數化類型

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

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

在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型。也就是說在泛型使用過程中,操作的數據類型被指定為一個參數,這種參數類型可以用在類、接口和方法中,分別被稱為泛型類、泛型接口、泛型方法。

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 特性

泛型只在編譯階段有效

  1. 在編譯的時候能夠檢查類型安全,並且所有的強制轉換都是自動和隱式的;
  2. 在邏輯上看以看成是多個不同的類型,實際上都是相同的基本類型。

二、泛型的使用

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

2.1 泛型類

泛型類型用於類的定義中,被稱為泛型類。通過泛型可以完成對一組類的操作對外開放相同的接口。最典型的就是各種容器類,如:ListSetMap

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;
    }
}

說明:

  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());
}
  • 不指定泛型類型

如果不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型可以為任何的類型。

@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方法要使用泛型能力,就必須使其成為泛型方法。

三、泛型通配符

我們在定義泛型類,泛型方法,泛型接口的時候經常會碰見很多不同的通配符,比如 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.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.1 示例源碼

Githu 示例代碼

5.2 參考文章

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

5.3 技術交流

Github 示例代碼

  1. 風塵博客:https://www.dustyblog.cn
  2. 風塵博客-掘金
  3. 風塵博客-博客園
  4. Github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM