前言
在 Java 中的 泛型,常常被稱之為 偽泛型,究其原因是因為在實際代碼的運行中,將實際類型參數(shù)的信息擦除掉了(Type Erasure)。那是什么原因?qū)е铝?Java 做出這種妥協(xié)的呢?下面我就帶著大家以 Java 語言設計者的角度,帶領大家一起了解這里面的辛酸過往。
什么是真泛型
在了解 Java "偽泛型" 之前,我們先簡單講一講"真泛型"與“偽泛型”的區(qū)別。
- 真泛型:泛型中的類型是真實存在的。
- 偽泛型:僅于編譯時類型檢查,在運行時擦除類型信息。
編程語言實現(xiàn)“真泛型”的一般思路
一般編程語言引入泛型的思路,都是通過編譯時膨脹法。
以 Java 語言實現(xiàn)"真泛型"為例,首先,對泛型類型(泛型類、泛型接口)、泛型方法的名字使用特別的編碼,例如將 Factory<T> 類生成為一個名為 “Factory@@T” 的類,這種特別的編碼后的名字將被編譯器識別,作為判斷是否為泛型的依據(jù)。方法中使用占位符 T 的地方可以臨時生成為 Object ,并帶上特別的 Annotation 讓 Java 編譯器能得知這個是占位符。然后,如果編譯時發(fā)現(xiàn)有對 Factory<String> 的使用,則將 “Factory@@T” 的所有邏輯復制一份,新建 “Factory@String@” 類,將原本的占位符 T 替換為 String 。然后在編譯 new Factory<String>() 時生成 new Factory@String@() 即可。
術語 中文含義 舉例 Parameterized type 參數(shù)化類型 List<String>Actual type parameter 實際類型參數(shù) StringFormal type parameter 形式類型參數(shù) E
以 Factory<String> 與 Factory<Integer> 為例,其生成的代碼為:
//替換前的代碼
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已經(jīng)被替換為 String。 - 其中
Factory@Integer@類中的T已經(jīng)被替換為 Integer。
因為含有不同的 實際類型參數(shù) 的 泛型類型 都被替換為了不同的類,且泛型類型中的類型也都得到了確認。所以我們在程序中可以這么做:
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% 能實現(xiàn)我們所說的 ”真泛型”。想要實現(xiàn)真泛型,有如下兩件事Java 必須要處理:
- 修改 JVM 源代碼,讓 JVM 能正確的的讀取和校驗泛型信息(之前的Java 是沒有泛型這種概念的,所以需要修改)。
- 為了兼容老程序,需為原本不支持泛型的 API 平行添加一套泛型 API(主要是容器類型)。
就拿 ArrayList 來說,也就是必須這么做:
- java.util.ArrayList ?? Java 老版本
- java.util.generic.
ArrayList<T>?? Java5 新增泛型版本
即使以上的事情都做了,Java 也并不能采用這種方案。試想如下情況:
Java 5 中引入了泛型

如果我有一個 Java 5 之下 的 A 項目與第三方的 B1 lib,其中有 A 項目中引用了 B1 lib 中的某個 ArrayList ,隨著 Java 的升級,B1 lib 的開發(fā)者為了使用 Java 新特性--泛型,故將代碼遷移到了 Java 5,并重新生成了 B2 lib,那么 A 項目要兼容 B2 lib,那么 A 項目中必須升級到 Java 5 并同時修改代碼。
A 項目為什么必須要升級 Java 5?
在 Java 中不支持高版本的 Java 編譯生成的 class 文件在低版本的 JRE 上運行,如果嘗試這么做,就會得到 UnsupportedClassVersionError 錯誤。如下圖所示:

故 A 項目要適配 B2 lib,必要要把 Java 升級到 Java 5。
那現(xiàn)在我們再回過頭來想想,Java 版本迭代都從 1.0 到 5.0了,有多少的開源框架,有多少項目,如果為了引入泛型,強行讓開發(fā)者修改代碼。這種情況,各位同學。自行腦補。估計數(shù)以萬計的開發(fā)者拿著刀,在堵 Java 語言架構師的門吧。
逼不得已的類型擦除
在上節(jié)中,我們探討了 Java 不能直接引入“真泛型” 的實際原因。因為“真泛型”的引入,勢必會為原本不支持泛型的 API 平行添加一套泛型 API。而新增了API,對于 Java 開發(fā)者來說,又必須要做遷移。
那還有什么方案,能讓開發(fā)者平滑的過渡到 Java 5, 又能使用泛型新特性呢?

有的,有的。Java 如果想擺脫用戶新版本的遷移問題。Java 必要要做以下兩件事情:
- 不再新增一套泛型 API,直接把已有的類型原地泛型化。
- 處理泛化前后類型的兼容。
下面我們分別探討一下這兩件事做的目的及其原因。
做第一件事,是保證了開發(fā)者不會因為 Java 的升級,而對以前的老代碼進行修改,以 ArrayList 為例,直接在原有包(java.util)下進行修改,也就是這樣:
//??Java老版本
class ArrayList{}
//??Java5泛型版本
class ArrayList<T>{}
做第二件事的目的,還是以我們之前的例子進行分析,在 A 項目中,A 項目引用了 B1 lib 中的 ArrayList(用 list 變量記錄),那么假設 A 項目升級到 Java 5 后,還是引用的 B1 lib,那么必然會出現(xiàn)如下這種情況:
下述代碼中,A 項目將泛化后
ArrayList<T>的傳遞給了 B1 lib 中的ArrayList。
ArrayList list = new ArrayList<String>();
- 左邊:B1 lib 中的老版本 ArrayList
- 右邊:A 項目 中的新泛型版本
ArrayList<T>
這種情況的出現(xiàn),會導致一個問題。就是 b1 項目中的 ArrayList 是不知道 A 項目中的 Arraylist 已經(jīng)泛型化了的,那么如何保證泛型化后的 ArrayList(也就是ArrayList<T>)與老版本的 ArrayList 等價呢?
如果按照我們之前講解的 “真泛型” 思路來處理 Java 的泛型, 那么 new ArrayList<String>() 實際會被替換為 new ArrayList@String@(),那么實際運行代碼是這樣:
ArrayList list = new ArrayList@String@()
從代碼邏輯上來看,根本就跑不通。因為 ArrayList 與 ArrayList@String@ 根本就不是同一類, 那怎么辦呢?
最為直接的解決方案就是,不再為參數(shù)化類型創(chuàng)造新類了,同時在編譯期間將泛型類型中的類型參數(shù)全部替換 Object(因為不創(chuàng)建新類了,那么在泛型類中的 T 對應的類型,只能用 Object 替換)。
在 Java 的泛型實際實現(xiàn)中,會根據(jù)泛型類型中的類型參數(shù)有無邊界,來選擇是否替換為邊界或 Object。
舉個例子:

在上述代碼中,聲明了一個泛型類型 Node<T>,在編譯器替換后,實際為 Node。也就是這樣:
//編譯器的代碼
Node node = new Node<String>();
//編譯后的代碼
Node node = new Node();
通過編譯器的”魔法“,Java 就解決了處理泛型兼容老版本的問題。
總結(jié)
閱讀到這里,我相信大家已經(jīng)明白了 Java 中的泛型為什么要擦除類型信息了。雖然 Java 的 "偽泛型“ 一直被其他編程語言所歧視,但不管怎樣,兼容老版本這種行為,也是一件值得尊敬以及認可的一件事。
不管 Java 做了什么,總是功大于過的。這里推薦一個視頻給大家。相信看了這個視頻之后,大家會知道 Java 對于整個世界的重要性。
最后
站在巨人的肩膀上,才能看的更遠~
- 泛型的內(nèi)部原理:類型擦除以及類型擦除帶來的問題
- Kotlin 的泛型
- Type Erasure
- State of the Specialization
- Java 不能實現(xiàn)真正泛型的原因是什么?
- Java 泛型官方文檔
- 為什么JVM上沒有C#語言?淺談Type Erasure特性
- 《Think in Java》
- 《Effective Java》