Java中的泛型 - 細節篇

前言

你們好啊,我是湯圓,今天給你們帶來的是《Java中的泛型 - 細節篇》,但願對你們有幫助,謝謝java

細心的觀衆朋友們可能發現了,如今的標題再也不是入門篇,而是各類詳細篇細節篇設計模式

是由於以前的幾篇比較簡單,因此叫作入門篇會合適點;數組

如今日後的都慢慢的開始複雜化了,因此叫入門就有點標題黨了,因此改叫詳細篇或者細節篇或者進階篇等等安全

文章純屬原創,我的總結不免有差錯,若是有,麻煩在評論區回覆或後臺私信,謝啦

簡介

泛型的做用就是把類型參數化,也就是咱們常說的類型參數工具

平時咱們接觸的普通方法的參數,好比public void fun(String s);參數的類型是String,是固定的測試

如今泛型的做用就是再將String定義爲可變的參數,即定義一個類型參數T,好比public static <T> void fun(T t);這時參數的類型就是T的類型,是不固定的this

從上面的String和T來看,泛型有着濃濃的多態的味道,但實際上泛型跟多態仍是有區別的spa

從本質上來說,多態是Java中的一個特性,一個概念,泛型是真實存在的一種類型;

目錄

下面咱們詳細說下Java中的泛型相關的知識點,目錄以下:設計

  • 什麼是類型參數
  • 爲啥要有泛型
  • 泛型的演變史
  • 類型擦除
  • 泛型的應用場景
  • 通配符限定
  • 動態類型安全
  • 等等
正文中大部分示例都是以集合中的泛型爲例來作介紹,由於用的比較多,你們都熟悉

正文

什麼是類型參數

類型參數就是參數的類型,它接受類做爲實際的值code

白話一點來講,就是你能夠把類型參數看做形參,把實際傳入的類看做實參

好比:ArrayList<E>中的類型參數E看作形參, ArrayList<String>中的類String看作實參

若是你學過工廠設計模式,那麼就能夠把這裏的ArrayList<E>看作一個工廠類,而後你須要什麼類型的ArrayList,就傳入對應的類型參數便可

  • 好比,傳入Integer則爲ArrayList<Integer>
  • 好比,傳入String則爲ArrayList<String>

爲啥要有泛型

主要是爲了提升代碼可讀性和安全性

具體的要從泛型的演變史提及

泛型的演變史

從廣義上來講,泛型很早就有了,只是隱式存在的;

好比List list = new ArrayList(); //等價於List<Object> list = new ArrayList<>();

可是這個時候的泛型是很脆弱的,可讀性和安全性都不好(這個時期的集合相對於數組來講,優點還不是很大)

首先,填充數據時,沒有類型檢查,那就有可能把Cat放到Dog集合中

其次,取出時,須要類型轉換,若是你很幸運的把對象放錯了集合(有多是故意的),那麼運行時就會報錯轉換異常(可是編譯卻能夠經過)

不過到了JDK1.5,出現了真正意義上的泛型(類型參數,用尖括號<>表示);

好比List<E>集合類,其中的E就是泛型的類型參數,由於集合中都是存的元素Element,因此用E字母替代(相似還有T,S,K-key,V-value);

這個時候,程序的健壯性就提升了,可讀性和安全性也都很高,看一眼就知道放進去的是個啥東西(這個時期的集合相對於數組來講,優點就很明顯了

如今拿List<Dog> list = new ArrayList<>();來舉例說明

首先,填充數據時,編譯器本身會進行類型檢查,防止將Cat放入Dog中

其次,取出數據時,不須要咱們手動進行類型轉換,編譯器本身會進行類型轉換

細心的你可能發現了,既然有了泛型,那我放進去的是Dog,取出的不該該也是Dog嗎?爲啥編譯器還要類型轉換呢?

這裏就引出類型擦除的概念

類型擦除

什麼是類型擦除?

類型擦除指的是,你在給類型參數<T>賦值時,編譯器會將實參類型擦除爲Object(這裏假設沒有限定符,限定符下面會講到)

因此這裏咱們要明白一個東西:虛擬機中沒有泛型類型對象的概念,在它眼裏全部對象都是普通對象

好比下面的代碼

擦除前

public class EraseDemo<T> {
    private T t;
    public static void main(String[] args) {
        
    }
    public T getT(){
        return t;
    }
    public void setT(T t){
        this.t = t;
    }
}

擦除後

public class EraseDemo {
    private Object t;
    public static void main(String[] args) {
        
    }
    public Object getT(){
        return t;
    }
    public void setT(Object t){
        this.t = t;
    }
}

能夠看到,T都變成了Object

泛型類被擦除後的類型,咱們通常叫它原始類型(raw type),好比EraseDemo<T>擦除後的原始類型就是EraseDemo

相應的,若是你有兩個數組列表,ArrayList<String>ArrayList<Integer> ,編譯器也會把二者都擦除爲ArrayList

你能夠經過代碼來測試一下

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());// 這裏會打印true
上面提到的限定符是幹嗎的?

限定符就是用來限定邊界的,若是泛型有設置邊界,好比<T extends Animal>,那麼擦除時,會擦除到第一個邊界Animal類,而不是Object

下面仍是以上面的代碼爲例,展現下擦除先後的對比

擦除前:

public class EraseDemo<T extends Animal> {
    private T t;
    public static void main(String[] args) {
        
    }
    public T getT(){
        return t;
    }
    public void setT(T t){
        this.t = t;
    }
}

擦除後:

public class EraseDemo {
    private Animal t;
    public static void main(String[] args) {
        
    }
    public Animal getT(){
        return t;
    }
    public void setT(Animal t){
        this.t = t;
    }
}
這裏的extends符號是表示繼承的意思嗎?

不是的,這裏的extends只是表示前者是後者的一個子類,能夠繼承也能夠實現

之因此用extends只是由於這個關鍵詞已經內置在Java中了,且比較符合情景

若是本身再造一個關鍵詞,好比sub,可能會使得某些舊代碼產生問題(好比使用sub做爲變量的代碼)

爲何要擦除呢?

這其實不是想不想擦除的問題,而是不得不擦除的問題

由於舊代碼是沒有泛型概念的,這裏的擦除主要是爲了兼容舊代碼,使得舊代碼和新代碼能夠互相調用

泛型的應用場景

  • 從大的方向來講:

    • 用在類中:叫作泛型類,類名後面緊跟<類型參數>,好比ArrayList<E>
    • 用在方法中:叫作泛型方法,方法的返回值前面添加<類型參數>,好比:public <T> void fun(T obj)
是否是想到了抽象類和抽象方法?

​ 仍是有區別的,抽象類和抽象方法是相互關聯的,可是泛型類和泛型方法之間沒有聯繫

  • 集中到類的方向來講:泛型多用在集合類中,好比ArrayList<E>

若是是自定義泛型的話,推薦用泛型方法,緣由有二:

  1. 脫離泛型類單獨使用,使代碼更加清晰(不用爲了某個小功能而泛化整個類)
  2. 泛型類中,靜態方法沒法使用類型參數;可是靜態的泛型方法能夠

通配符限定

這裏主要介紹<T>, <? extends T>, <? super T>的區別

  • <T>:這個是最經常使用的,就是普通的類型參數,在調用時傳入實際的類來替換T便可,這個實際的類能夠是T,也能夠是T的子類
好比 List<String> list = new ArrayList<>();,這裏的String就是實際傳入的類,用來替換類型參數T
  • <? extends T>:這個屬於通配符限定中的子類型限定,即傳入實際的類必須是T或者T子類

乍一看,這個有點像<T>類型參數,都是往裏放T或者T的子類;

可是區別仍是挺多的,後面會列出

  • <? super T>:這個屬於通配符限定中的超類型限定,即傳入實際的類必須是T或者T的父類
  • <?>:這個屬於無限定通配符,即它也不知道里面該放啥類型,因此乾脆就不讓你往裏添加,只能獲取(這一點相似<? extends T>

下面用表格列出<T><? extends T>, <? super T>的幾個比較明細的區別

<T> <? extends T> <? super T>
類型擦除 傳入實參時,實參被擦爲Object,可是在get時編譯器會自動轉爲T 擦到T 擦到Object
引用對象 不能將引用指向子類型或者父類型的對象,好比:List<Animal> list = new ArrayList<Cat>();//報錯 能將引用指向子類型的對象,好比:List<? extends Animal> list = new ArrayList<Cat>(); 能將引用指向父類型的對象,好比:List<? super Cat> list = new ArrayList<Animal>();
添加數據 能夠添加數據,T或者T的子類 不能 能,T或者T的子類

下面咱們用代碼來演示下

類型擦除:

// <T>類型,傳入實參時,擦除爲Object,可是get時仍是實參的類型
List<Animal> list1 = new ArrayList<>();// 合法
list1.add(new Dog());// 合法
Animal animal = list1.get(0); // 這裏不須要強轉,雖然前面傳入實參時被擦除爲Object,可是get時編譯器內部已經作了強制類型轉換

// <? extends T> 子類型的通配符限定,擦除到T(整個過程再也不變)
List<? extends Animal> list2 = list1;// 合法
Animal animal2 = list2.get(0); // 這裏不須要強轉,由於只擦除到T(即Animal)

// <? super T> 超類型的通配符限定,擦除到Object
List<? super Animal> list3 = list1; // 合法
Animal animal3 = (Animal)list3.get(0); // 須要手動強制,由於被擦除到Object

將引用指向子類型或父類型的對象:

// <T>類型,不能指向子類型或父類型
List<Animal> list = new ArrayList<Dog>();// 報錯:須要的是List<Animal>,提供的是ArrayList<Dog>

// <? extends T> 子類型的通配符限定,指向子類型
List<? extends Animal> list2 = new ArrayList<Dog>();// 合法

// <? super T> 超類型的通配符限定,指向父類型
List<? super Dog> list3 = new ArrayList<Animal>(); // 合法

添加數據

// <T>類型,能夠添加T或者T的子類型
List<Animal> list1 = new ArrayList<>();
list.add(new Dog());// 合法

// <? extends T> 子類型的通配符限定,不能添加元素
List<? extends Animal> list2 = new ArrayList<Dog>();// 正確
list2.add(new Dog()); // 報錯:不能往裏添加元素

// <? super T> 超類型的通配符限定,能夠添加T或者T的子類型
List<? super Dog> list3 = new ArrayList<Animal>();
list3.add(new Dog()); // 合法,能夠添加T類型的元素
list3.add(new Animal());//報錯,不能添加父類型的元素

下面針對上面的測試結果進行解惑

先從<T>的報錯開始吧

爲啥 <T>類型的引用不能指向子類型,好比 List<Animal> list = new ArrayList<Dog>();

首先說明一點,Animal和Dog雖然是父子關係(Dog繼承Animal),可是List<Animal> List<Dog>之間是沒有任何關係的(有點像Java和Javascript)

他們之間的關係以下圖

T引用

之因此這樣設計,主要是爲了類型安全的考慮

下面用代碼演示,假設能夠將List<Animal>指向子類List<Dog>

List<Animal> list = new ArrayList<Dog>();// 假設這裏不報錯
list.add(new Cat()); //這裏把貓放到狗裏面了

第二行能夠看到,很明顯,把貓放到狗裏面是不對的,這就又回到了泛型真正出現以前的時期了(沒有泛型,集合存取數據時不安全)

那爲啥 <? extends T>就能指向子類型呢?好比 List<? extends Animal> list = new ArrayList<Dog>();

說的淺一點,緣由是:這個通配符限定出現的目的就是爲了解決上面的不能指向子類的問題

固然,這個緣由說了跟沒說同樣。下面開始正經點解釋吧

由於這個通配符限定不容許插入任何數據,因此當你指向子類型時,這個list就只能存放指向的那個集合裏的數據了,而不能再往裏添加;

天然的也就類型安全了,只能訪問,不能添加

爲何 <? extends T>不容許插入數據呢?

其實這個的緣由跟上面的修改引用對象是相輔相成的,合起來就是爲了保證泛型的類型安全性

考慮下面的代碼

List<Animal> list = new ArrayList<>();
list.add(new Cat());
list.add(new Dog());
Dog d = (Dog) list.get(0); // 報錯,轉換異常

能夠看到,插入的子類很混亂,致使提取時轉型容易出錯(這是泛型<T>的一個弊端,固然咱們寫的時候多用點心可能就不會這個問題)

可是有了<? extends T>以後,就不同了

首先你能夠經過修改引用的對象來使得list指向不一樣的Animal子類

其次你添加數據,不能直接添加,可是能夠經過指向的Animal子類對象來添加

這樣就保證了類型的安全性

代碼以下:

// 定義一個Dog集合
List<Dog> listDog = new ArrayList<>();
listDog.add(new Dog());

// 讓<? extends Animal>通配符限定的泛型 指向上面的Dog集合
List<? extends Animal> list2 = listDog;
// 這時若是想往裏添加數據,只須要操做listDog便可,它能夠保證類型安全
listDog.add(new Dog());
// 若是本身去添加,就會報錯
list2.add(new Dog());// 報錯

<? extends T>通常用在形參,這樣咱們須要哪一個子類型,只須要傳入對應子類的泛型對象就能夠了,從而實現泛型中的多態

<? super T>爲啥能夠插入呢?

兩個緣由

  1. 它只能插入T或者T的子類
  2. 它的下限是T

也就是說你隨便插入,我已經限制了你插入的類型爲T或者T的子類

那麼我在查詢時,就能夠放心的轉爲T或者T的父類

代碼以下:

List<? super Dog> listDog = new ArrayList<>();
listDog.add(new Dog());
listDog.add(new Cat()); // 報錯
listDog.add(new Animal()); // 報錯
Dog dog = (Dog) listDog.get(0); // 內部被擦除爲Object,因此要手動強轉
爲啥 <T>獲取時,編譯器會自動強轉轉換,到了這裏 <? super T>,就要手動轉換了呢?

這個多是由於編譯器也不肯定你的要返回的T的父類是什麼類型,因此乾脆留給你本身來處理了

可是若是你把這個listDog指向一個父類的泛型對象,而後又在父類的泛型對象中,插入其餘類型,那可就亂了(又回到<T>的問題了,要本身多注意)

好比:

List<Animal> list = new ArrayList<>();
list.add(new Cat()); // 加了Cat
// 指向Animal
List<? super Dog> listDog = list;
listDog.add(new Dog());
list.add(new Cat()); // 報錯
list.add(new Animal()); // 報錯

Dog dog = (Dog) listDog.get(0); //報錯:轉換異常Cat-》Dog

因此建議<? super T>在添加數據的時候,儘可能集中在一個地方,不要多個地方添加,像上面的,要麼都在<? super Dog>裏添加數據,要麼都在<Animal>中添加

動態類型安全檢查

這個主要是爲了跟舊代碼兼容,對舊代碼進行的一種類型安全檢查,防止將Cat插入Dog集合中這種錯誤

這種檢查是發生在編譯階段,這樣就能夠提前發現問題

對應的類爲Collections工具類,方法以下圖

類型安全檢查

代碼以下

// 動態類型安全檢查,在與舊代碼兼容時,防止將Dog放到Cat集合中相似的問題

// === 檢查以前 ===
List list = new ArrayList<Integer>();
// 添加不報錯
list.add("a");
list.add(1);
// 只有用的時候,纔會報錯
Integer i = (Integer) list.get(0); // 這裏運行時報錯

// === 檢查以後 ===
List list2 = Collections.checkedList(new ArrayList<>(), Integer.class);
// 插入時就會報錯
list2.add("a"); // 這裏編譯時就報錯,提早發現錯誤
list2.add(1);

總結

泛型的做用:

  1. 提升類型安全性:預防各類類型轉換問題
  2. 增長程序可讀性:所見即所得,看獲得放進去的是啥,也知道會取出啥
  3. 提升代碼重用性:多種同類型的數據(好比Animal下的Dog,Cat)能夠集合到一處來處理,從而調高代碼重用性

類型擦除:

​ 泛型T在傳入實參時,實參的類型會被擦除爲限定類型(即<? extends T>中的T),若是沒有限定類型,則默認爲Object

通配符限定:

  1. <? extends T>:子類型的通配符限定,以查詢爲主,好比消費者集合場景
  2. <? super T>:超類型的通配符限定,以添加爲主,好比生產者集合場景

後記

最後,感謝你們的觀看,謝謝

相關文章
相關標籤/搜索