1. 程式人生 > >用了這麼多年的 Java 泛型,你對它到底有多瞭解?

用了這麼多年的 Java 泛型,你對它到底有多瞭解?

> 本篇文章 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)