大話"爪哇"泛型

  在某個不知名的方位有這麼一個叫作爪哇的島國,得天獨厚的天然條件使得該國物產豐富,其中因爲盛產著名的爪哇咖啡而聞名世界,每一年都吸引大批來自世界各地的遊客前往觀光學習,觀光能夠理解,學習這怎麼個理解法,帶着這個疑問咱們繼續往下深刻,嗶嗶,開車!html

  頗有意思的是,在爪哇國中生活的當地居民都使用 Java 語言溝通交流,這種叫作 Java 的語言是由最初爪哇國第一代國王所創造出來的,由此逐漸完善並演變爲文化文字、交流語言,一直傳承至今。由於有了語言交流,也使得信息傳達更加及時,這爲該國生產咖啡豆(Java Bean) 的行業提供很是大的幫助,因此該國的咖啡豆出口需求量一直很大,也正所以爪哇島的國旗是一杯熱氣騰騰的咖啡:java

泛型的出現

  任何樣東西的推出都不是一下就完美的,玉石通過巧匠的雕琢後溫潤有方,上鋪的室友任性辭職後,回家繼承家產現有車有房,只留下我原地感慨:高級玩家!數組

  還在公元 JDK 1.4 年代的時候,那個時候尚未泛型的概念,當時的人們都是相似於下面交流:安全

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("https://www.talkmoney.cn/");
    list.add(5);
    String str = (String)list.get(0);
    Integer num = (Integer)list.get(1);
}
複製代碼

  首先能夠看到的是聲明瞭一個集合,而後往集合裏放入不一樣的數據,而且在取出數據的時候還要作強制類型轉換,此外人們還會由於存入集合中第幾個是什麼類型的數據而煩惱,就會形成取出數據強轉的時候形成轉換異常ClassCastException,這就比如於加工商從種植園來的貨車上卸下不一樣品種的原料,致使最後所烘培加工的咖啡豆並非所須要的。多線程

  這種狀況等到爪哇國第五代君主上任纔有所改變,新任君主勵精圖治並針對此狀況頒佈泛型這條法規,通常的類和方法,只能使用具體的類型:要麼是基本類型,要麼是自定義的類。若是要編寫能夠應用於多種類型的代碼,這種限制就會對代碼的束縛就會很大,事實上,泛型是爲了讓編寫的代碼能夠被不一樣類型的對象重用,因而人們今後交流的方式又變爲了如此:app

public static void main(String[] args) {
    List<String> list = new ArrayList();
    list.add("https://www.talkmoney.cn/");
    list.add("你們好,我是蘆葦");
    String url = list.get(0);
    String name = list.get(1);
}
複製代碼

  這時候建立集合併爲其指定了 String 類型,也就是說如今只能往集合中存入 String 類型的數據,也正所以,在取出數據的時候也不須要再進行強制轉換的操做,從而避免了類型轉換異常的風險,而在爪哇國的"憲法"《Thinking in Java》中提出,泛型出現的緣由在於:ide

有許多緣由促成泛型的出現,其中最引人注意的一個緣由,就是爲了建立容器類。學習

  仔細想一想,彷佛集合類庫和泛型還真的有點配,這裏以 Collection 接口爲例:this

public interface Collection<E> extends Iterable<E> {

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);
    
    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);
}   
複製代碼

泛型的應用

  前面提到泛型是爲了讓編寫的代碼能夠被不一樣類型的對象重用,那麼就須要給操做的數據類型指定參數,這種參數類型可使用在類、方法、集合、接口等,下面就分別來看看。url

泛型類

  假如如今咱們須要一個能夠經過傳入不一樣類型數據的類,經過使用泛型能夠這樣來編寫:

public class GenericClass<T> {
    private T item;
    
    public void setItem(T t) {
        this.item = t;
    }
    
    public void getItem(T t) {
        return this.item;
    }
}
複製代碼

  咋一看能夠發現泛型類和普通類差很少,區別在於類名後面多了個類型參數列表,尖括號內的 T 爲指定的類型參數,這裏不必定非得是 T,能夠本身任意命名。指定參數後,能夠看到類中的成員,方法及參數均可以使用這個參數類型,而且最後返回的都仍是這個指定的參數類型,來看它的使用:

public static void main(String[] args) {
    GenericClass<String> name = new GenericClass<>("蘆葦科技");
    GenericClass<Integer> age = new GenericClass<>(5);
}
複製代碼

泛型方法

  泛型方法顧名思義就是使用泛型的方法,它有本身的類型參數,一樣它的參數也是在尖括號間,而且位於方法返回值以前,此外,泛型方法能夠存在於泛型類或者普通類中,以下:

// 泛型類
public class GenericClass<T> {
    private T item;
    
    public void setItem(T t) {
        this.item = t;
    }
    // 只是用到了泛型並無聲明類型參數,因此不要混淆此爲泛型方法
    public void getItem(T t) {
        return this.item;
    }
    
    // 泛型方法
    public <T> void GenericMethod(T t) {
        System.out.println(t);
    }
}
複製代碼

  這裏須要注意的是,泛型方法中的類型 T 和泛型類中的類型 T 是兩碼事,雖然它們都是 T,但其實它們之間互相不影響,此外類型參數能夠有多個。

泛型接口

  泛型接口的定義基本和泛型類的定義是差很少的,類型參數寫在接口名後,以下:

public interface GenericInterface<T> {
    public void doSomething(T t);
}
複製代碼

  在使用中,實現類在實現泛型接口時須要傳入具體的參數類型,那麼以後使用到泛型的地方都會被替換傳入的參數類型,以下:

public class Generic implements GenericInterface<String> {
    @override
    public void doSomething(String s) {
     	......   
    }
}
複製代碼

泛型通配符

  除去在前面介紹完的泛型類、方法等以外,泛型還有其餘方面的應用,例若有時候但願傳入的類型參數有一個限定的範圍內,這一點爪哇國顯然也早就料到,解決方案即是泛型通配符,那麼泛型通配符主要有如下三種類型:

  • 上界通配符 <? extends T>
  • 下界通配符 <? super T>
  • 無界通配符 <?>

  在瞭解通配符以前,首先咱們得要對類型信息有個基本的瞭解。

public class Coffee {}

// 卡布奇諾咖啡
public class Cappuccino extends Coffee {}

// 第一種建立方式
Cappuccino cappuccino = new Cappuccino();
// 第二種建立方式
Coffee cappuccino = new Cappuccino();
複製代碼

  上面這個類中有一個 Coffee 類,Coffee 類是 Cappuccino 類的父類,往下即是兩種建立類對象的方式,第一種方式很常見,建立 cappuccino 對象並指向 Cappuccino 類對象,這樣在 JVM 編譯時仍是運行時的類型信息都是 Cappuccino 類型。那麼第二種方式呢,咱們使用 Coffee 類型的變量指向 Cappuccino 類型對象,這在 Java 的語法中稱爲向上轉型,在 Java 中能夠把子類對象賦值給父類對象。運行時類型信息可讓咱們在程序運行時發現並使用類型信息,而全部的類型轉換都是在運行時識別對象的類型,因此在編譯時的類型是 Coffee 類型,而在運行時 JVM 經過初始化對象發現它指向 Cappuccino 類型對象,因此運行時它是 Cappuccino 類型。


上界通配符 <? extends T>

  大概理清了下類型信息後,前面咱們也提到過有時候但願傳入的類型參數有一個限定的範圍內,那麼在前面的基礎上定義一個 Cup 類用來裝載咖啡:

public class Cup<T> {
    private List<T> list;
    public void add(T t) {list.add(t);}
    public T get(int index) {return list.get(index);}
}
複製代碼

  這裏這個 Cup 類是一個泛型類,而且指明類型參數 T,表明着咱們能夠傳入任何類型,假如目前須要一個裝咖啡的杯子,理論上既然是裝咖啡的杯子,那麼就能夠裝上卡布奇諾,由於卡布奇諾也是屬於咖啡的一種啊對吧,以下:

Cup<Coffee> cup = new Cup<Cappuccino>();	// compiler error
複製代碼

  然而咱們會發現代碼在編譯時會發生錯誤,雖然知道 Coffee 類和 Cappuccino 類存在繼承關係,可是在泛型中是不支持這樣的寫法,解決辦法就是經過上界通配符來處理:

Cup<? extends Coffee> cup = new Cup<Cappuccino>();
複製代碼

  在類型參數列表中使用 extends 表示在泛型中必須是 Coffee 或者是其子類,固然在類型參數列表中還能夠有多個參數,能夠用逗號對它們進行隔開。經過上界通配符解決了這個問題,可是使用上界通配符會使得沒法往其中存聽任何對象,卻能夠從中取出對象

Cup<? extends Coffee> cup = new Cup<Cappuccino>();
cup.add(new Cappuccino()); 			  // compiler error
Coffee coffee = cup.get(0);		          // compiler success
複製代碼

  出現這種狀況的緣由,咱們知道一個 Cup<? extends Coffee> 的引用,可能指向 Coffee 類對象,也能夠指向其餘 Coffee 類的子類對象,這樣的話 JVM 在編譯時並不能肯定具體是什麼類型,而爲了安全考慮,就直接一棒子打死。而從中取出數據,最後都能經過向上轉型使用 Coffee 類型變量去引用它。因此,當咱們使用上界通配符 <? extends T> 的時候,須要注意的是不能往其中插入,可是能夠讀取;

下界通配符 <? super T>

  下界通配符 <? super T> 的特性則恰好與上界通配符 <? extends T> 相反,即只能往其中存入 T 或者其子類的數據,可是在讀取的時候就會受到限制

Cup<? super Cappuccino> cup = new Cup<Coffee>();         // compiler success
Cup<? super Cappuccino> cup = new Cup<Object>(); 	 // compiler success
Object object = cup.get(0);
複製代碼

  對於 cup 來講,它指向的具體類型能夠是 Cappuccino 類的任何父類,雖然沒法知道具體是哪一個類型,可是它均可以轉化爲 T 類型,因此能夠往其中插入 T 及其子類的數據,而在讀取方面,由於裏面存儲的都是T 及其基類,沒法轉型爲任何一種類型,只有經過 Object 基類去取出數據。

無界通配符 <?>

  若是單獨使用 則使用的是無界通配符,若是不肯定實際要操做的類型參數,則可使用該通配符,它能夠持有任何類型,這裏須要注意的是,咱們很容易將 和 搞混,例如 List 和 List 咋看之下彷佛非常相像,實際上卻不是這樣的,List 是一個未知類型的 List,而 List 則是任意類型的 List,咱們能夠將 List 賦值給 List<?>,卻不能把 List 賦值給 List,要搞清楚這一點:

List<Object> objectList = new ArrayList<>();
List<?> anyTypeList;
List<String> stringList = new ArrayList<>();

anyTypeList = stringList;	       // compiler success
objectList = (List<Object>)stringList; // compiler error
複製代碼

通配符小結

  經過前面的瞭解,無界通配符 <?> 用於表示不肯定限定範圍的場景下,而對於使用上界通配符 <? extends T> 和下界通配符 <? super T> 也知道它們的使用和受限制的地方:

  • 上界通配符 <? extends T> 不能插入存儲數據,卻能夠讀取數據,適合須要內容讀取的場景;
  • 下界通配符 <? super T> 則能夠插入 T 類型及其子類對象,而只能經過 Object 變量去取出數據,適合須要內容插入的場景;

  實際上,這種狀況又被叫作 PECS 原則,PECS 的全稱是 Producer Extends Consumer Super。Producer Extends 說明的是當須要獲取內容資源去生產時,此時的場景角色是生產者,可使用上界通配符 <? extends T> 更好地讀取數據;Consumer Super 則指的是當咱們須要插入內容資源以待消費,此時的場景角色是消費者,可使用下界通配符 <? super T> 更好地插入數據。具體的選擇能夠根據本身的實際場景須要靈活選擇。說到生產者消費者,感受又回到初識多線程那會兒,時間就這樣悄悄地溜走,捉也捉不住。

泛型的神祕之處

關於泛型機制

  在金庸老爺子描繪的江湖世界中,裏面有種武功絕學叫作乾坤大挪移,只要習得此功能夠直接施展對方武功,哪怕是現學現用都過之而不及,聽起來泛型彷佛也差很少。那麼先從一個例子提及:

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        ArrayList<Integer> b = new ArrayList<Integer>();
        System.out.println(a.getclass() == b.getclass());
    }
}
// 運行結果:true
複製代碼

  儘管這是兩個不一樣類型的集合數組,當獲取它們的類信息並比較的時候,此時的二者之間的類型居然是同樣的,究其緣由這是 Java 泛型擦除的機制,首先須要明白的是,泛型是體如今編譯的時候實現,以後在生成的字節碼文件和運行時是不包含泛型信息的,因此類型信息是會被擦除掉的,只留下原始類型,這個過程就是泛型擦除的機制,之因此擦除是爲了兼容以前原有的代碼。此外,關於原始類型下面再展開敘述。

  上面提到泛型被擦除後只保留原始類型,那麼這個原始類型啥東東,以下:

// 擦除前
public class GenericClass<T> {
    private T data;
    
    public T getData() {return data;}
    ......
}

// 擦除後
public class GenericClass {
    private Object data;
    
    public Object getData() {return data;}
    ......
}
複製代碼

  對於沒有指定界限類型參數,在被類型擦除以後會被替換爲 Object,這是相對於無界類型參數而言,如上述所言,若是沒有指定限制範圍,那麼類型參數就會被替換爲 Object,那若是要讓類型限定在一個範圍內的狀況呢?

// 擦除前
public class GenericClass<T extends Serializable> {
  	private T data;
    public T getData() {return data;}
    ......
}

// 擦除後
public class GenericClass {
    private Serializable data;
    public Serializable getData() {return data;}
    ......
}
複製代碼

  那麼此時的狀況是有界類型參數,類型擦除後會替換爲指定的邊界,固然這裏的邊界能夠指定多個,若是在有多個邊界的狀況下,那麼類型參數也只是會擦除第一個邊界。

泛型的本質

  說到這裏,咱們可能就會存在這樣一個疑問,既然說泛型在類編譯的時候會被擦除,那麼在運行時是如何作到插入讀取類型一致的呢?換句話來講就是,泛型類型參數被擦除爲原始類型 Object,那麼按理來講是能夠插入其餘類型的數據的啊!

  事實上,JVM 雖然在對類進行編譯的時候將類型參數進行擦除,可是它會保證使用到泛型類型參數的一致性。咱們知道,在類還處於編譯階段的時候,此時的類型參數還能夠獲取的到,編譯器能夠作類型檢查,舉例來講就是,一個 ArrayList 被聲明爲 Integer 的時候,當往該 ArrayList 中插入數據的時候會對其進行判斷,以此保證數據類型的一致性。而當類型參數被擦除後,爲了可以保證從中讀取數據的類型是原來指定的類型參數,JVM 默默地幫咱們進行類型轉換,以此將類型參數還原成咱們指定的那個它~

泛型的限制

  因爲 Java 泛型擦除機制,指定的類型參數會被擦除,因此對於如下的一些操做將是不容許的:

  • 不能使用類型參數進行建立實例;
  • 不能使用類型參數進行 instanceof;
public class GenericClass<T> {
    public static void method(Object obj) {
        if (obj instanceof T) { // compiler error
            ......
        }
        T t = new T(); // compiler error
    }
}
複製代碼
  • 泛型指定的類型參數不能是基本類型
ArrayList<int> list = new ArrayList<int>(); // compiler error
/** * 可使用基本數據類型的包裝類 */
ArrayList<Integer> list = new ArrayList<Integer>(); // compiler success
複製代碼
  • 不能建立類型參數的數組
ArrayList<Integer>[] list = new ArrayList<Integer>(3);
複製代碼

結語

  能夠看到,爪哇國爲了能讓國民安居樂業,也是下了一番苦心,這些年來隨着「咖啡市場"的供不該求,不少人也都加入了進來,在文章的開頭中也提到,每一年都吸引大批來自世界各地的遊客前往觀光學習,將來到底怎麼樣誰也不知道,只是不少時候就像一座圍城,有的人出來,有的人進去。用他們的一句話來講:」唉啥,混口飯吃!「。

  到這裏,本文已經進入尾聲,關於泛型這一方面還有許多未能詳細記錄,但願也能在這裏起到個拋磚引玉的做用,因爲本人水平有限還請批評指正。


參考:


內推信息

  • 咱們正在招募小夥伴,有興趣的小夥伴能夠把簡歷發到 app@talkmoney.cn,備註:來自掘金社區
  • 詳情能夠戳這裏--> 廣州蘆葦信息科技
相關文章
相關標籤/搜索