用了這麼多年的 Java 泛型,你對它到底有多瞭解?
阿新 • • 發佈:2020-05-22
> 本篇文章 idea 來自[用了這麼多年的泛型,你對它到底有多瞭解?](https://www.cnblogs.com/huangxincheng/p/12764925.html),恰好當時看了「深入 Java 虛擬機器的第三版」瞭解泛型的一些歷史,感覺挺有意思的,就寫了寫 Java 版的泛型。
作為一個 Java 程式設計師,日常程式設計早就離不開泛型。泛型自從 JDK1.5 引進之後,真的非常提高生產力。一個簡單的泛型 **T**,寥寥幾行程式碼, 就可以讓我們在使用過程中動態替換成任何想要的型別,再也不用實現繁瑣的型別轉換方法。
雖然我們每天都在用,但是還有很多同學可能並不瞭解其中的實現原理。今天這篇我們從以下幾點聊聊 Java 泛型:
- Java 泛型實現方式
- 型別擦除帶來的缺陷
- Java 泛型發展史
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071801220-685847296.png)
> 點贊再看,養成習慣,微信搜尋『程式通事』。
> [點選檢視更多相關文章](https://sourl.cn/WhjNLb)
## Java 泛型實現方式
Java 採用**型別擦除(Type erasure generics)**的方式實現泛型。用大白話講就是這個泛型只存在原始碼中,編譯器將原始碼編譯成位元組碼之時,就會把泛型『**擦除**』,所以位元組碼中並不存在泛型。
對於下面這段程式碼,編譯之後,我們使用 `javap -s class` 檢視位元組碼。
![方法原始碼](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071801441-336325336.jpg)
![位元組碼](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071801646-1183272088.jpg)
觀察`setParam` 部分的位元組碼,從 `descriptor` 可以看到,泛型 **T** 已被擦除,最終替換成了 `Object`。
> ps:並不是每一個泛型引數被擦除型別後都會變成 Object 類,如果泛型型別為 T extends String 這種方式,最終泛型擦除之後將會變成 String。
同理`getParam` 方法,泛型返回值也被替換成了 `Object`。
為了保證 `String param = genericType.getParam();` 程式碼的正確性,編譯器還得在這裡插入型別轉換。
除此之外,編譯器還會對泛型安全性防禦,如果我們往 `ArrayList` 新增 `Integer`,程式編譯期間就會報錯。
最終型別擦除後的程式碼等同與如下:
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071801872-436022436.jpg)
## 型別擦除帶來的缺陷
作為對比,我們再來簡單聊下 **C#** 泛型的實現方式。
**C#**泛型實現方式為「**具現化式泛型(Reifiable generics)**」,不熟悉的 **C#**小夥伴可以不用糾結**具現化**技術概念,我也不瞭解這些特性--!
簡單點來講,**C#**實現的泛型,無論是在程式原始碼,還是在編譯之後的,甚至是執行期間都是切實存在的。
相對比與 **C#** 泛型,Java 泛型看起來就像是個「**偽**」泛型。Java 泛型只存在程式原始碼中,編譯之後就被擦除,這種缺陷相應的會帶來一些問題。
### 不支援基本資料型別
泛型引數被擦除之後,強制變成了 `Object` 型別。這麼做對於引用型別來說沒有什麼問題,畢竟 `Object` 是所有型別的父型別。但是對於 `int/long` 等八個基本資料型別說,這就難辦了。因為 Java 沒辦法做到`int/long` 與 `Object` 的強制轉換。
如果要實現這種轉換,需要進行一系列改造,改動難度還不小。所以當時 Java 給出一個簡單粗暴的解決方案:既然沒辦法做到轉換,那就索性不支援原始型別泛型了。
如果需要使用,那就規定使用相關包裝類的泛型,比如 `ArrayList`。另外為了開發人員方便,順便增加了原生資料型別的**自動拆箱/裝箱**的特性。
正是這種「偷懶」的做法,導致現在我們沒辦法使用原始型別泛型,又要忍受包裝類裝箱/拆箱帶來的開銷,從而又帶來執行效率的問題。
### 執行效率
上面位元組碼例子我們已經看到,泛型擦除之後型別將會變成 `Object`。當泛型出現在方法輸入位置的時候,由於 Java 是可以向上轉型的,這裡並不需要強制型別轉換,所以沒有什麼問題。
但是當泛型引數出現在方法的輸出位置(返回值)的時候,呼叫該方法的地方就需要進行向下轉換,將 `Object` 強制轉換成所需型別,所以編譯器會插入一句 `checkcast` 位元組碼。
除了這個,上面我們還說到原始基本資料型別,編譯器還需幫助我們進行裝箱/拆箱。
所以對於下面這段程式碼來說:
```java
List list = new ArrayList();
list.add(66); // 1
int num = list.get(0); // 2
```
對於①處,編譯器要做就是增加基本型別的裝箱即可。但是對於第二步來說,編譯器首先需要將 `Object` 強制轉換成 `Integer`,接著編譯器還需要進行拆箱。
型別擦除之後,上面程式碼等同於:
```java
List list = new ArrayList();
list.add(Integer.valueOf(66));
int num = ((Integer) list.get(0)).intValue();
```
如果上面泛型程式碼在 C# 實現,就不會有這麼多額外步驟。所以 Java 這種型別擦除式泛型實現方式無論使用效果與執行效率,還是全面落後於 C# 的具現化式泛型。
### 執行期間無法獲取泛型實際型別
由於編譯之後,泛型就被擦除,所以在程式碼執行期間,Java 虛擬機器無法獲取泛型的實際型別。
下面這段程式碼,從原始碼上兩個 List 看起來是不同型別的集合,但是經過泛型擦除之後,集合都變為 `ArrayList`。所以 `if`語句中程式碼將會被執行。
```java
ArrayList li = new ArrayList();
ArrayList lf = new ArrayList();
if (li.getClass() == lf.getClass()) { // 泛型擦除,兩個 List 型別是一樣的
System.out.println("6666");
}
```
這樣程式碼看起來就有點反直覺,這對新手來說不是很友好。
另外還會給我們在實際使用中帶來一些限制,比如說我們沒辦法直接實現以下程式碼:
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071802013-650189725.jpg)
最後再舉個例子,比如說我們需要實現一個泛型 `List` 轉換成陣列的方法,我們就沒辦法直接從 List 去獲取泛型實際型別,所以我們不得不額外再傳入一個 Class 型別,指定陣列的型別:
```java
public static E[] convert(List list, Class componentType) {
E[] array = (E[]) Array.newInstance(componentType, list.size());
....
}
```
從上面的例子我們可以看到,Java 採用型別擦除式實現泛型,缺陷很多。那為什麼 Java 不採用 C# 的那種泛型實現方式?或者說採用一種更好實現方式?
這個問題等我們瞭解 Java 泛型機制的歷史,以及當時 Java 語言的現狀,我們才能切身體會到當時 Java 採用這種泛型實現方式的原因。
## Java 泛型歷史背景
Java 泛型最早是在 JDK5 的時候才被引入,但是泛型思想最早來自來自 C++ 模板(template)。1996 年 Martin Odersky(Scala 語言締造者) 在剛釋出的 Java 的基礎上擴充套件了泛型、函數語言程式設計等功能,形成一門新的語言-「**Pizza**」。
後來,Java 核心開發團隊對 **Pizza** 的泛型設計深感興趣,與 Martin 合作,一起合作開發的一個新的專案「**Generic Java**」。這個專案的目的是為了給 Java 增加泛型支援,但是不引入函數語言程式設計等功能。最終成功在 Java5 中正式引入泛型支援。
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071802173-1703805257.jpg)
泛型移植過程,一開始並不是朝著型別擦除的方向前進,事實 **Pizza** 中泛型更加類似於 **C#** 中的泛型。
但是由於 Java 自身特性,自帶嚴格的約束,讓 Martin 在**Generic Java** 開發過程中,不得不放棄了 Pizza 中泛型設計。
這個特性就是,Java 需要做到**嚴格的向後相容性**。也就是說一個在 JDK1.2 編譯出來 Class 檔案,不僅能在 JDK 1.2 能正常執行,還得必須保證在後續 JDK,比如 JDK12 中也能保證正常的執行。
這種特性是明確寫入 Java 語言規範的,這是一個對 Java 使用者的一個嚴肅承諾。
> 這裡強調一下,這裡的向後相容性指的是二進位制相容性,並不是原始碼相容性。也不保證高版本的 Class 檔案能夠執行在低版本的 JDK 上。
現在困難點在於,Java 1.4.2 之前都沒有支援泛型,而 Java5 之後突然要支援泛型,還要讓 JDK1.4 之前編譯的程式能在新版本中正常執行,這就意味著以前沒有的限制,就不能突然增加。
舉個例子:
```java
ArrayList arrayList=new ArrayList();
arrayList.add("6666");
arrayList.add(Integer.valueOf(666));
```
沒有泛型之前, List 集合是可以儲存不同型別的資料,那麼引入泛型之後,這段程式碼必須的能正確執行。
為了保證這些舊的 Clas 檔案能在 Java5 之後正常執行,設計者基本有兩條路:
1. 需要泛型化的容器(主要是容器型別),以前有的保持不變,平行增加一套新的泛型化的版本。
2. 直接把已有的型別原地泛型化,不增加任何新的已有型別的泛型版本。
如果 Java 採用第一條路實現方式,那麼現在我們可能就會有兩套集合型別。以 `ArrayList` 為例,一套為普通的 `java.util.ArrayList`,一套可能為 `java.util.generic.ArrayList`。
採用這種方案之後,如果開發中需要使用泛型特性,那麼直接使用新的型別。另外舊的程式碼不改動,也可以直接執行在新版本 JDK 中。
這套方案看起來沒什麼問題,實際上C# 就是採用這套方案。但是為什麼 Java 卻沒有使用這套方案那?
這是因為當時 C# 才釋出兩年,歷史程式碼並不多,如果舊程式碼需要使用泛型特性,改造起來也很快。但是 Java 不一樣,當時 Java 已經發布十年了,已經有很多程式已經執行部署在生產環境,可以想象歷史程式碼非常多。
如果這些應用在新版本 Java 需要使用泛型,那就需要做大量原始碼改動,可以想象這個開發工作量。
另外 Java 5 之前,其實我們就已經有了兩套集合容器,一套為 `Vector/Hashtable` 等容器,一套為 `ArrayList/ HashMap`。這兩套容器的存在,其實已經引來一些不便,對於新接觸的 Java 的開發人員來說,還得學習這兩者的區別。
如果此時為了泛型再引入新型別,那麼就會有四套容器同時並存。想想這個畫面,一個新接觸開發人員,面對四套容器,完全不知道如何下手選擇。如何 Java 真的這麼實現了,想必會有更多人吐槽 Java。
所以 Java 選擇第二條路,採用型別擦除,只需要改動 Javac 編譯器,不需要改動位元組碼,不需要改動虛擬機器,也保證了之前歷史沒有泛型的程式碼還可以在新的 JDK 中執行。
但是第二條路,並不代表一定需要使用型別擦除實現,如果有足夠時間好好設計,也許會有更好的方案。
當年留下的技術債,現在只能靠 **Valhalla** 專案來還了。這個專案從2014 年開始立項,原本計劃在 JDK10 中解決現有語言的各種缺陷。但是結果我們也知道了,現在都 JDK14 了,還只是完成少部分木目標,並沒有解決核心目標,可見這個改動的難度啊。
## 總結
本文我們先從 Java 泛型底層實現方式開始聊起,接著舉了幾個例子,讓大家瞭解現在泛型實現方式存在一些缺陷。
然後我們帶入 Java 泛型歷史背景,站在 Java 核心開發者的角度,才能瞭解 Java 泛型這麼現實無奈原因。
最後作為 Java 開發者,讓我們對於現在 Java 一些不足,少些抱怨,多一些理解吧。相信之後 Java 核心開發人員肯定會解決泛型現有的缺陷,讓我們拭目以待。
## 幫助資料
1. https://www.zhihu.com/question/38940308
2.
3. https://hllvm-group.iteye.com/group/topic/25910
4. http://blog.zhaojie.me/2010/05/why-java-sucks-and-csharp-rocks-4-generics.html
5. http://blog.zhaojie.me/2010/04/why-java-sucks-and-csharp-rocks-2-primitive-types-and-object-orientation.html
6. https://en.wikipedia.org/wiki/Generics_in_Java
7. https://www.zhihu.com/question/34621277/answer/59440954
8. https://www.artima.com/scalazine/articles/origins_of_scala.html
## 最後(求關注,求點贊,求轉發)
本文是在看了『深入 Java虛擬機器(第三版)』之後,知道 Java 泛型這些故事,才有本篇文章。
首先感謝一下機械工業出版社的小哥哥的贈書。
剛開始知道『深入 Java虛擬機器(第三版)』釋出的時候,本來以為只是對第二版稍微補充而已。等收到這本書的時候,才發現自己錯了。兩本書放在一起,完全就不是一個量級的。
> ps:盜取一張 Why 神的圖
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071802463-1715665335.jpg)
第三本在第二版的基礎增加大量補充,也解決了第二版留下一些沒解釋的問題。所以沒買的同學,推薦直接購買第三版。
兩個版本的具體區別,大家可以看下 Why 神的的文章,這篇文章還被本書的作者打賞過哦。
[深入 Java虛擬機器兩版比較 ](https://mp.weixin.qq.com/s/rYVDgttWw9WQA1IF3KJjUQ)
我是樓下小黑哥,一個還未禿的程式猿,我們下週三見~
![](https://img2020.cnblogs.com/other/1419561/202005/1419561-20200522071802698-675131007.jpg)
> 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyidea.cn)