在某個不知名的方位有這麼一個叫作爪哇的島國,得天獨厚的天然條件使得該國物產豐富,其中因爲盛產著名的爪哇咖啡而聞名世界,每一年都吸引大批來自世界各地的遊客前往觀光學習,觀光能夠理解,學習這怎麼個理解法,帶着這個疑問咱們繼續往下深刻,嗶嗶,開車!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) {
......
}
}
複製代碼
除去在前面介紹完的泛型類、方法等以外,泛型還有其餘方面的應用,例若有時候但願傳入的類型參數有一個限定的範圍內,這一點爪哇國顯然也早就料到,解決方案即是泛型通配符,那麼泛型通配符主要有如下三種類型:
在瞭解通配符以前,首先咱們得要對類型信息有個基本的瞭解。
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 類型。
大概理清了下類型信息後,前面咱們也提到過有時候但願傳入的類型參數有一個限定的範圍內,那麼在前面的基礎上定義一個 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> 的特性則恰好與上界通配符 <? 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> 也知道它們的使用和受限制的地方:
實際上,這種狀況又被叫作 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 泛型擦除機制,指定的類型參數會被擦除,因此對於如下的一些操做將是不容許的:
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);
複製代碼
能夠看到,爪哇國爲了能讓國民安居樂業,也是下了一番苦心,這些年來隨着「咖啡市場"的供不該求,不少人也都加入了進來,在文章的開頭中也提到,每一年都吸引大批來自世界各地的遊客前往觀光學習,將來到底怎麼樣誰也不知道,只是不少時候就像一座圍城,有的人出來,有的人進去。用他們的一句話來講:」唉啥,混口飯吃!「。
到這裏,本文已經進入尾聲,關於泛型這一方面還有許多未能詳細記錄,但願也能在這裏起到個拋磚引玉的做用,因爲本人水平有限還請批評指正。
參考:
Thinking in Java
- 咱們正在招募小夥伴,有興趣的小夥伴能夠把簡歷發到 app@talkmoney.cn,備註:來自掘金社區
- 詳情能夠戳這裏--> 廣州蘆葦信息科技