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

Van_Fan發表於2020-01-19

一、概述

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
  5. 公眾號
    風塵部落格

相關文章