爲何Java的泛型要用"擦除"實現

前言

在 Java 中的 泛型,經常被稱之爲 僞泛型,究其緣由是由於在實際代碼的運行中,將實際類型參數的信息擦除掉了(Type Erasure)。那是什麼緣由致使了 Java 作出這種妥協的呢?下面我就帶着你們以 Java 語言設計者的角度,帶領你們一塊兒瞭解這裏面的辛酸過往。html

什麼是真泛型

在瞭解 Java "僞泛型" 以前,咱們先簡單講一講"真泛型"與「僞泛型」的區別。java

  • 真泛型:泛型中的類型是真實存在的。
  • 僞泛型:僅於編譯時類型檢查,在運行時擦除類型信息。

編程語言實現「真泛型」的通常思路

通常編程語言引入泛型的思路,都是經過編譯時膨脹法node

以 Java 語言實現"真泛型"爲例,首先,對泛型類型(泛型類、泛型接口)泛型方法的名字使用特別的編碼,例如將 Factory<T> 類生成爲一個名爲 「Factory@@T」 的類,這種特別的編碼後的名字將被編譯器識別,做爲判斷是否爲泛型的依據。方法中使用佔位符 T 的地方能夠臨時生成爲 Object ,並帶上特別的 Annotation 讓 Java 編譯器能得知這個是佔位符。而後,若是編譯時發現有對 Factory<String> 的使用,則將 「Factory@@T」 的全部邏輯複製一份,新建 「Factory@String@」 類,將本來的佔位符 T 替換爲 String 。而後在編譯 new Factory<String>() 時生成 new Factory@String@() 便可。編程

術語 中文含義 舉例
Parameterized type 參數化類型 List<String>
Actual type parameter 實際類型參數 String
Formal type parameter 形式類型參數 E

Factory<String>Factory<Integer> 爲例,其生成的代碼爲:api

//替換前的代碼
Factory<String>() f1 = new Factory<String>();
Factory<Integer>() f2 = new Factory<Integer>();

//替換後的代碼
Factory<String>() f1 = new Factory@String@()
Factory<Integer>() f2 = new Factory@Integer@();
複製代碼
  • 其中 Factory@String@ 類中的 T 已經被替換爲 String。
  • 其中 Factory@Integer@ 類中的 T 已經被替換爲 Integer。

由於含有不一樣的 實際類型參數泛型類型 都被替換爲了避免同的類,且泛型類型中的類型也都獲得了確認。因此咱們在程序中能夠這麼作:架構

class Factory<T> {
    T data;
    public static void f(Object arg){
        if(arg instanceof T){...}//success
        T var = new T();//success
        T[] array = new T[10];//success
    }
}
複製代碼

Java 直接使用 「真泛型」 帶來的問題

單從技術來講,Java 是徹底 100% 能實現咱們所說的 」真泛型」。想要實現真泛型,有以下兩件事Java 必需要處理:oracle

  • 修改 JVM 源代碼,讓 JVM 能正確的的讀取和校驗泛型信息(以前的Java 是沒有泛型這種概念的,因此須要修改)。
  • 爲了兼容老程序,需爲本來不支持泛型的 API 平行添加一套泛型 API(主要是容器類型)。

就拿 ArrayList 來講,也就是必須這麼作:框架

  • java.util.ArrayList 👈 Java 老版本
  • java.util.generic.ArrayList<T> 👈 Java5 新增泛型版本

即便以上的事情都作了,Java 也並不能採用這種方案。試想以下狀況:jvm

Java 5 中引入了泛型編程語言

如何兼容老版本.png

若是我有一個 Java 5 之下 的 A 項目與第三方的 B1 lib,其中有 A 項目中引用了 B1 lib 中的某個 ArrayList ,隨着 Java 的升級,B1 lib 的開發者爲了使用 Java 新特性--泛型,故將代碼遷移到了 Java 5,並從新生成了 B2 lib,那麼 A 項目要兼容 B2 lib,那麼 A 項目中必須升級到 Java 5 並同時修改代碼。

A 項目爲何必需要升級 Java 5?

在 Java 中不支持高版本的 Java 編譯生成的 class 文件在低版本的 JRE 上運行,若是嘗試這麼作,就會獲得 UnsupportedClassVersionError 錯誤。以下圖所示:

版本說明.png

故 A 項目要適配 B2 lib,必要要把 Java 升級到 Java 5。

那如今咱們再回過頭來想一想,Java 版本迭代都從 1.0 到 5.0了,有多少的開源框架,有多少項目,若是爲了引入泛型,強行讓開發者修改代碼。這種狀況,各位同窗。自行腦補。估計數以萬計的開發者拿着刀,在堵 Java 語言架構師的門吧。

逼不得已的類型擦除

在上節中,咱們探討了 Java 不能直接引入「真泛型」 的實際緣由。由於「真泛型」的引入,勢必會爲本來不支持泛型的 API 平行添加一套泛型 API。而新增了API,對於 Java 開發者來講,又必需要作遷移。

那還有什麼方案,能讓開發者平滑的過渡到 Java 5, 又能使用泛型新特性呢?

抽菸分析.jpg

有的,有的。Java 若是想擺脫用戶新版本的遷移問題。Java 必要要作如下兩件事情:

  • 再也不新增一套泛型 API,直接把已有的類型原地泛型化
  • 處理泛化先後類型的兼容。

下面咱們分別探討一下這兩件事作的目的及其緣由。

作第一件事,是保證了開發者不會由於 Java 的升級,而對之前的老代碼進行修改,以 ArrayList 爲例,直接在原有包(java.util)下進行修改,也就是這樣:

//👇Java老版本
class ArrayList{}

//👇Java5泛型版本
class ArrayList<T>{}
複製代碼

作第二件事的目的,仍是以咱們以前的例子進行分析,在 A 項目中,A 項目引用了 B1 lib 中的 ArrayList(用 list 變量記錄),那麼假設 A 項目升級到 Java 5 後,仍是引用的 B1 lib,那麼必然會出現以下這種狀況:

下述代碼中,A 項目將泛化後 ArrayList<T> 的傳遞給了 B1 lib 中的 ArrayList

ArrayList list = new ArrayList<String>();
複製代碼
  • 左邊:B1 lib 中的老版本 ArrayList
  • 右邊:A 項目 中的新泛型版本 ArrayList<T>

這種狀況的出現,會致使一個問題。就是 b1 項目中的 ArrayList 是不知道 A 項目中的 Arraylist 已經泛型化了的,那麼如何保證泛型化後的 ArrayList(也就是ArrayList<T>)與老版本的 ArrayList 等價呢?

若是按照咱們以前講解的 「真泛型」 思路來處理 Java 的泛型, 那麼 new ArrayList<String>() 實際會被替換爲 new ArrayList@String@(),那麼實際運行代碼是這樣:

ArrayList list = new Factory@String@()
複製代碼

從代碼邏輯上來看,根本就跑不通。由於 ArrayListArrayList@String@ 根本就不是同一類, 那怎麼辦呢?

最爲直接的解決方案就是,再也不爲參數化類型創造新類了,同時在編譯期間將泛型類型中的類型參數所有替換 Object(由於不建立新類了,那麼在泛型類中的 T 對應的類型,只能用 Object 替換)。

在 Java 的泛型實際實現中,會根據泛型類型中的類型參數有無邊界,來選擇是否替換爲邊界或 Object。

舉個例子:

編譯器替換.png

在上述代碼中,聲明瞭一個泛型類型 Node<T>,在編譯器替換後,實際爲 Node。也就是這樣:

//編譯器的代碼
 Node node = new Node<String>();
 //編譯後的代碼
 Node node = new Node();
複製代碼

經過編譯器的」魔法「,Java 就解決了處理泛型兼容老版本的問題。

總結

閱讀到這裏,我相信你們已經明白了 Java 中的泛型爲何要擦除類型信息了。雖然 Java 的 "僞泛型「 一直被其餘編程語言所歧視,但無論怎樣,兼容老版本這種行爲,也是一件值得尊敬以及承認的一件事。

無論 Java 作了什麼,老是功大於過的。這裏推薦一個視頻給你們。相信看了這個視頻以後,你們會知道 Java 對於整個世界的重要性。

最後

站在巨人的肩膀上,才能看的更遠~

相關文章
相關標籤/搜索