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

来源:https://www.cnblogs.com/vandusty/archive/2020/01/18/12198770.html

一、概述 泛型( )是 中引入的一個新特性, 泛型提供了 編譯時類型安全檢測機制 ,該機制允許開發者在編譯時檢測到非法的類型。 1.1 什麼是泛型? 泛型,即 參數化類型 。 一提到參數,最熟悉的就是定義方法時有形參,然後調用此方法時傳遞實參。那麼參數化類型怎麼理解呢?顧名思義,就是將類型由原來的具 ...


一、概述

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

您的分享是我們最大的動力!

更多相關文章
  • 一、HashMap(JDK1.8) 1、基本知識、數據結構 (1)時間複雜度:用來衡量演算法的運行時間。 參考:https://blog.csdn.net/qq_41523096/article/details/82142747 (2)數組:採用一段連續的存儲空間來存儲數據。查找方便,增刪麻煩。 (3 ...
  • 基礎知識掃盲 對稱加密 對稱密鑰加密 , 又叫私鑰加密。即信息發送的方和接受方用一個密鑰去加密和揭秘數據。 最大的優勢是 加解密速度快,適合對大量數據進行加密, 對稱加密的缺點是密鑰的管理和分配, 換句話說就是 如何把密鑰發送到需要解密你的消息的人手裡的問題。在發送密鑰的過程中, 密鑰有很大的風險被 ...
  • 深度優先搜索(DFS)和廣度優先搜索(BFS)是基本的暴力技術,常用於解決圖、樹的遍歷問題。 首先考慮演算法思路。以老鼠走迷宮為例: (1):一隻老鼠走迷宮。它在每個路口都選擇先走右邊,直到碰壁無法繼續前進,然後回退一步,這一次走左邊,接著繼續往下走。用這個辦法能走遍所有的路,而且不會重覆。這個思路就 ...
  • [toc] 1、創建虛擬環境 2、安裝Django 3、創建Django工程 4、運行Django項目 5、訪問Django服務 如果出現以下效果表示項目運行成功 到此項目就算創建完成了 ...
  • 究竟是真“自主”,還是又一個披著“洋”皮的“紅芯瀏覽器”? ​ 作者 | 沉迷單車的追風少年 出品 | CSDN博客 昨天看到新聞: ! ​ 心頭一震,看起來很厲害啊!畢竟前幾天美國宣佈要對中國AI軟體進行限制: ​ 這是要還一巴掌的節奏啊。頓時來了興趣,趕緊下載一個嘗嘗鮮。 網上很多類似的新聞,都 ...
  • 新年將至,年味漸濃。 美團點評技術年貨如期而至。 從2013年12月4日發佈第一篇文章,一直到今天,美團技術團隊官方博客已經走過了6個春秋。 由衷地感謝大家一直以來對我們的鼓勵和陪伴! 2020年春節到來之際,我們精選美團技術博客幾十篇技術乾貨以及數篇國際頂會論文,整理製作成一本厚達900多頁的電子 ...
  • 由於下學期要學習JavaEE所以打算將JavaSE的知識再重新學習一遍,打好基礎的同時也希望自己有新的收穫和更深刻的理解。 這次複習主要是參考 "廖雪峰老師的java教程" ,每學習完一章對其中一些要點進行總結和概括。 簡介 Java最早是由SUN公司(已被Oracle收購)的 "詹姆斯·高斯林" ...
  • 一.基礎方式的增刪該查: 1.mybatis約定:輸入參數parameterType和輸出參數resulrType在形式上只能有一個。 2.如果輸入/輸出參數:是簡單類型(8個基本類型加String)則可以使用任何占位符,#{xxx}; 如果是對象類型,則必須是對象的屬性,#{屬性名}。 3.輸出參 ...
一周排行
  • 比如要拆分“呵呵呵90909086676喝喝999”,下麵當type=0返回的是中文字元串“呵呵呵,喝喝”,type=1返回的是數字字元串“90909086676,999”, private string GetStrings(string str,int type=0) { IList<strin ...
  • Swagger一個優秀的Api介面文檔生成工具。Swagger可以可以動態生成Api介面文檔,有效的降低前後端人員關於Api介面的溝通成本,促進項目高效開發。 1、使用NuGet安裝最新的包:Swashbuckle.AspNetCore。 2、編輯項目文件(NetCoreTemplate.Web.c ...
  • 2020 年 7 月 30 日, 由.NET基金會和微軟 將舉辦一個線上和為期一天的活動,包括 微軟 .NET 團隊的演講者以及社區的演講者。本次線上大會 專註.NET框架構建微服務,演講者分享構建和部署雲原生應用程式的最佳實踐、模式、提示和技巧。有關更多信息和隨時瞭解情況:https://focu... ...
  • #abp框架Excel導出——基於vue #1.技術棧 ##1.1 前端採用vue,官方提供 UI套件用的是iview ##1.2 後臺是abp——aspnetboilerplate 即abp v1,https://github.com/aspnetboilerplate/aspnetboilerp ...
  • 前言 本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。 作者:碧茂大數據 PS:如有需要Python學習資料的小伙伴可以加下方的群去找免費管理員領取 input()輸入 Python提供了 input() 內置函數從標準輸入讀入一 ...
  • 從12年到20年,python以肉眼可見的趨勢超過了java,成為了當今It界人人皆知的編程語言。 python為什麼這麼火? 網路編程語言搜索指數 適合初學者 Python具有語法簡單、語句清晰的特點,這就讓初學者在學習階段可以把精力集中在編程對象和思維方法上。 大佬都在用 Google,YouT ...
  • 在社會上存在一種普遍的對培訓機構的學生一種歧視的現象,具體表現在,比如:當你去公司面試的時候,一旦你說了你是培訓機構出來的,那麼基本上你就涼了,那麼你瞞著不說,然後又通過了面試成功入職,但是以後一旦在公司被髮現有培訓經歷,可能會面臨被降薪,甚至被辭退,培訓機構出來的學生,在用人單位眼裡就是能力低下的 ...
  • from typing import List# 這道題看了大佬寫的代碼,經過自己的理解寫出來了。# 從最外圍的四周找有沒有為O的,如果有的話就進入深搜函數,然後深搜遍歷# 判斷上下左右的位置是否為Oclass Solution: def solve(self, board: List[List[s ...
  • import requests; import re; import os; # 1.請求網頁 header = { "user-agent":'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, li ...
  • import requests; import re; import os; import parsel; 1.請求網頁 header = { "user-agent":'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537. ...