泛型的基本原理

泛型是 JDK1.5 的一個新特性,其實就是一個『語法糖』,本質上就是編譯器爲了提供更好的可讀性而提供的一種小「手段」,虛擬機層面是不存在所謂『泛型』的概念的。java

在我看來,『泛型』的存在具備如下兩點意義,這也是它被設計出來的初衷。git

一是,經過泛型的語法定義,編譯器能夠在編譯期提供必定的類型安全檢查,過濾掉大部分由於類型不符而致使的運行時異常,例如:程序員

ArrayList<Integer> list = new ArrayList<>();
list.add("ddddd"); //編譯失敗
複製代碼

因爲咱們的 ArrayList 是符合泛型語法定義的容器,因此你能夠在實例化的時候指定一個類型,限定該容器只能容納 Integer 類型的元素。而若是你強行添加其餘類型的元素進入,那麼編譯器是不會經過的。github

二是,泛型可讓程序代碼的可讀性更高,而且因爲自己只是一個語法糖,因此對於 JVM 運行時的性能是沒有任何影響的。數組

固然,『泛型』也有它與身俱來的一些缺點,雖然看起來好像只是提供了一種類型安全檢查的功能,可是實際上這種語法糖的實現卻沒有看起來的那樣輕鬆,理解好泛型的基本原理將有助於你理解各種容器集合框架。安全

類型擦除

『類型擦除』的概念放在最開始進行介紹是爲了方便你們初步創建起對於『泛型』的一個基本認識,從而對於後續介紹的使用方式上會更容易理解。bash

泛型這種語法糖,編譯器會在編譯期間「擦除」泛型語法並相應的作出一些類型轉換動做。例如:微信

public class Caculate<T> {

    private T num;
}
複製代碼

咱們定義了一個泛型類,具體定義泛型類的細節待會會進行詳細介紹,這裏關注咱們的類型擦除過程。定義了一個屬性成員,該成員的類型是一個泛型類型,這個 T 具體是什麼類型,咱們也不知道,它只是用於限定類型的。框架

固然,咱們也能夠反編譯一下這個 Caculate 類:性能

public class Caculate{

    public Caculate(){}

    private Object num;
}
複製代碼

會獲得這樣一個結果,很明顯的是,編譯器擦除 Caculate 類後面的兩個尖括號,而且將 num 的類型定義爲 Object 類型。

固然,有人可能就會問了,「是否是全部的泛型類型都以 Object 進行擦除呢?」

答案是:大部分狀況下,泛型類型都會以 Object 進行替換,而有一種狀況則不是。

public class Caculate<T extends String> {

    private T num;
}
複製代碼

這種狀況的泛型類型,num 會被替換爲 String 而再也不是 Object。

這是一個類型限定的語法,它限定 T 是 String 或者 String 的子類,也就是你構建 Caculate 實例的時候只能限定 T 爲 String 或者 String 的子類,因此不管你限定 T 爲何類型,String 都是父類,不會出現類型不匹配的問題,因而可使用 String 進行類型擦除。

那麼不少人也會有這樣的疑問,你類型擦除以後,全部泛型相關方法的返回值都是 Object,那我當初泛型限定的具體類型還有用嗎?例如這樣一個方法:

ArrayList<Integer> list = new ArrayList();
list.add(10);
Integer num = list.get(0);
複製代碼
//這是 ArrayList 內部的一個方法
public E get(int index) {
    .....
}
複製代碼

就是說,你類型擦除以後,方法 get 的返回值 E 會被擦除爲 Object 類型,那麼爲何咱們看到的確實返回的 Integer 類型呢?

image

這是上述三行代碼的一個反編譯結果,能夠看到,實際上編譯器會正常的將 ArrayList 編譯並進行類型擦除,而後返回實例。可是除此以外的是,若是構建 ArrayList 實例時使用了泛型語法,那麼編譯器將標記該實例並關注該實例後續全部方法的調用,每次調用前都進行安全檢查,非指定類型的方法都不能調用成功。

其實還有一點可能你們都不多關注,大多數人只是知道編譯器會類型擦除一個泛型類並對建立出來的實例進行必定的安全檢查。可是實際上編譯器不只關注一個泛型方法的調用,它還會爲某些返回值爲限定的泛型類型的方法進行強制類型轉換,因爲類型擦除,返回值爲泛型類型的方法都會擦除成 Object 類型,當這些方法被調用後,編譯器會限定這個結果只能賦值給 Integer 或者 Object。

其實這一個過程,咱們管它叫作『泛型翻譯』。不得不感嘆一下,編譯器爲了矇騙虛擬機對程序員提供泛型服務但是沒少費心思啊。

泛型的基本使用

泛型類與接口

定義一個泛型類或接口是容易的,咱們看幾個 JDK 中的泛型類。

  • public class ArrayList
  • public interface List
  • public interface Queue

基本格式是這樣的:

訪問修飾符 class/interface 類名或接口名<限定類型變量名>
複製代碼

其中「限定類型變量名」能夠是任意一個變量名稱,你叫它 T 也好,E 也好,只要符合 Java 變量命名規範就能夠。在這裏至關於聲明瞭一個泛型限定類型,該類中的成員屬性或者方法均可以直接拿來用。

泛型方法

這裏你們須要明確一點的是,泛型方法並不必定依賴其外部的類或者接口,它能夠獨立存在,也能夠依賴外圍類存在。例如:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
複製代碼

ArrayList 的這個 get 方法就是一個泛型方法,它依賴外圍 ArrayList 聲明的 E 這個泛型類型,也就是它沒有本身聲明一個泛型類型而用的外圍類的。

固然,另外一種方式就是本身申明一個泛型類型並使用:

public class Caculate {

    public <T> T add(T num){
        return num;
    }
}
複製代碼

這是泛型方法的另外一種形態,其中 <T> 用於聲明一個名稱爲 T 的泛型類型,第二個 T 是方法的返回值。

因此外部調用該方法都須要指定一個限定類型才能調用,像這樣:

Caculate caculate = new Caculate();
caculate.<Integer>add(12);
caculate.<String>add("fadf");
複製代碼

使用泛型的目的就是爲了限定類型,原本不使用泛型語法,那麼全部的參數都是 Object 類型的,如今泛型容許咱們限定具體類型,這一點要明確。

固然,你們可能沒怎麼見過這樣的調用語法,不管是平常寫代碼,或是看 JDK 源碼實現裏,基本上都省略了類型限定部分,也就是上述代碼等效於:

Caculate caculate = new Caculate();
caculate.add(12);
caculate.add("fadf");
複製代碼

爲何呢?由於編譯會推斷你的參數類型,因此容許你省略,但前提是你這個方法是有參數的,若是你這個方法的邏輯是不須要傳參的,那麼你依然須要顯式指定限定的具體類型。例如:

public class Caculate {

    public <T> T add(){
        T num = null;
        return num;
    }
}
複製代碼
Caculate caculate = new Caculate();
caculate.add();
複製代碼

這樣的 add 方法調用,就意味着你沒有限定 T 的類型,那麼這個 T 實際上就是 Object 類型,並無被限定。

泛型的類型限定

這裏的類型限定其實指的是這麼個語法:

<T extends String>
複製代碼

它既能夠應用於泛型類或者接口的定義上,也能夠應用在泛型方法的定義上,它聲明瞭一個泛型的類型 T,而且 T 類型必須是 String 或者 String 的子類,也就是外部使用時所傳入的具體限定類型不能是非 String 體系的類型。

使用這種語法時,因爲編譯器會確保外部使用時傳入的具體限定類型不會超過 String,因此在編譯期間將再也不使用 Object 作類型擦除,可使用 String 進行類型擦除。

通配符

通配符是用於解決泛型之間引用傳遞問題的特殊語法。看下面一段代碼:

public static void main(String[] args){
    Integer[] integerArr = new Integer[2];
    Number[] numberArr = new Number[2];
    numberArr = integerArr;

    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<Number> numbers = new ArrayList<>();
    numbers = integers;//編譯不經過
}
複製代碼

Java 中,數組是協變的,即 Integer extends Number,那麼子類數組實例是能夠賦值給父類數組實例的。那是因爲 Java 中的數組類型本質上會由虛擬機運行時動態生成一個類型,這個類型除了記錄數組的必要屬性,如長度,元素類型等,會有一個指針指向內存某個位置,這個位置就是該數組元素的起始位置。

因此子類數組實例賦值父類數組實例,只不過意味着父類數組實例的引用指向堆中子類數組而已,並不會有所衝突,所以是 Java 容許這種操做的。

而泛型是不容許這麼作的,爲何呢?

咱們假設泛型容許這種協變,看看會有什麼問題。

ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;//假設的前提下,編譯器是能經過的
numbers.add(23.5);
複製代碼

假設 Java 容許泛型協變,那麼上述代碼在編譯器看來是沒問題的,但運行時就會出現問題。這個 add 方法實際上就將一個浮點數放入了整型容器中了,雖然因爲類型擦除並不會對程序運行形成問題,但顯然違背了泛型的設計初衷,容易形成邏輯混亂,因此 Java 乾脆禁止泛型協變。

因此雖然 ArrayList<Integer> 和 ArrayList<Number>編譯器類型擦除以後都是 ArrayList 的實例,可是起碼在編譯器看來,這二者是兩種不一樣的類型。

那麼,假若有某種需求,咱們的方法既要支持子類泛型做爲形參傳入,也要支持父類泛型做爲形參傳入,又該怎麼辦呢?

咱們使用通配符處理這樣的需求,例如:

public void test2(ArrayList<? extends Number> list){
        
}
複製代碼

ArrayList<? extends Number> 表示泛型類型具體是什麼不知道,可是具體類型必須是 Number 及其子類類型。例如:ArrayList<Number>,ArrayList<Integer>,ArrayList<Double> 等。

可是,通配符每每用於方法的形參中,而不容許用於定義和調用語法中。例以下面的語句是不被支持的:

ArrayList<?> list = new ArrayList<>();
複製代碼

固然了,除了 <? extends xxx> 這種通配符,還有另外兩種:

  • :通配任意一種類型
  • :必須是某個類型的父類

通配符至關於一個集合,符合通配符描述的類型都被框進集合中,方法調用時傳入的實參都必須是這個集合中的一員,不然將不能經過編譯。

細節與侷限

通配符的只讀性

考慮這樣一段代碼:

ArrayList<Number> list = new ArrayList<>();
ArrayList<?> arrayList = list;
arrayList.add(32);
arrayList.add("fadsf");
arrayList.add(new Object());
複製代碼

上述的三條 add 語句都不能經過編譯,這就是通配符的一個侷限點,通配符匹配出來的泛型類型只能讀取,不能寫。

緣由也很簡單,? 表明不肯定類型,即你不知道你這個容器裏面放的是什麼類型的數據,因此你只能讀取裏面的數據,不能瞎往裏面添加元素。

泛型不容許建立數組

咱們剛開始介紹通配符的時候說過,數組具備協變性,即子類數組實例能夠賦值給父類數組實例。咱們也說過,泛型類型不具備協變性,即使兩個泛型類實例的具體類型是父子關係,他們之間也不能相互轉換。

具體緣由是什麼,咱們也詳細介紹了,大體意思就是,父類容器能夠聽任意類型的元素,而子類容器只能放某種特殊類型的元素,若是父類表明了某一個子類容器,那麼父類容器就有可能放入非當前子類實例所容許的元素進入容器,這會致使邏輯上的混亂,因此 Java 不容許這麼作。

那麼,若是容許泛型建立數組,因爲數組的協變性,泛型數組必然也具備協變性,而泛型自己又不容許協變,天然衝突,因此泛型數組也是不容許建立的。


文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公衆號:OneJavaCoder,全部文章都將同步在公衆號上。

image
相關文章
相關標籤/搜索