做爲一個 Java 程序員,平常編程早就離不開泛型。泛型自從 JDK1.5 引進以後,真的很是提升生產力。一個簡單的泛型 T,寥寥幾行代碼, 就可讓咱們在使用過程當中動態替換成任何想要的類型,不再用實現繁瑣的類型轉換方法。html
雖然咱們天天都在用,可是還有不少同窗可能並不瞭解其中的實現原理。今天這篇咱們從如下幾點聊聊 Java 泛型:java
Java 採用類型擦除(Type erasure generics)的方式實現泛型。用大白話講就是這個泛型只存在源碼中,編譯器將源碼編譯成字節碼之時,就會把泛型『擦除』,因此字節碼中並不存在泛型。程序員
對於下面這段代碼,編譯以後,咱們使用 javap -s class
查看字節碼。編程
觀察setParam
部分的字節碼,從 descriptor
能夠看到,泛型 T 已被擦除,最終替換成了 Object
。數組
ps:並非每個泛型參數被擦除類型後都會變成 Object 類,若是泛型類型爲 T extends String 這種方式,最終泛型擦除以後將會變成 String。
同理getParam
方法,泛型返回值也被替換成了 Object
。安全
爲了保證 String param = genericType.getParam();
代碼的正確性,編譯器還得在這裏插入類型轉換。ide
除此以外,編譯器還會對泛型安全性防護,若是咱們往 ArrayList<String>
添加 Integer
,程序編譯期間就會報錯。函數式編程
最終類型擦除後的代碼等同與以下:函數
做爲對比,咱們再來簡單聊下 C# 泛型的實現方式。學習
C#泛型實現方式爲「具現化式泛型(Reifiable generics)」,不熟悉的 C#小夥伴能夠不用糾結具現化技術概念,我也不瞭解這些特性--!
簡單點來說,C#實現的泛型,不管是在程序源碼,仍是在編譯以後的,甚至是運行期間都是切實存在的。
相對比與 C# 泛型,Java 泛型看起來就像是個「僞」泛型。Java 泛型只存在程序源碼中,編譯以後就被擦除,這種缺陷相應的會帶來一些問題。
泛型參數被擦除以後,強制變成了 Object
類型。這麼作對於引用類型來講沒有什麼問題,畢竟 Object
是全部類型的父類型。可是對於 int/long
等八個基本數據類型說,這就難辦了。由於 Java 沒辦法作到int/long
與 Object
的強制轉換。
若是要實現這種轉換,須要進行一系列改造,改動難度還不小。因此當時 Java 給出一個簡單粗暴的解決方案:既然沒辦法作到轉換,那就索性不支持原始類型泛型了。
若是須要使用,那就規定使用相關包裝類的泛型,好比 ArrayList<Integer>
。另外爲了開發人員方便,順便增長了原生數據類型的自動拆箱/裝箱的特性。
正是這種「偷懶」的作法,致使如今咱們沒辦法使用原始類型泛型,又要忍受包裝類裝箱/拆箱帶來的開銷,從而又帶來運行效率的問題。
上面字節碼例子咱們已經看到,泛型擦除以後類型將會變成 Object
。當泛型出如今方法輸入位置的時候,因爲 Java 是能夠向上轉型的,這裏並不須要強制類型轉換,因此沒有什麼問題。
可是當泛型參數出如今方法的輸出位置(返回值)的時候,調用該方法的地方就須要進行向下轉換,將 Object
強制轉換成所需類型,因此編譯器會插入一句 checkcast
字節碼。
除了這個,上面咱們還說到原始基本數據類型,編譯器還需幫助咱們進行裝箱/拆箱。
因此對於下面這段代碼來講:
List<Integer> list = new ArrayList<Integer>(); list.add(66); // 1 int num = list.get(0); // 2
對於①處,編譯器要作就是增長基本類型的裝箱便可。可是對於第二步來講,編譯器首先須要將 Object
強制轉換成 Integer
,接着編譯器還須要進行拆箱。
類型擦除以後,上面代碼等同於:
List list = new ArrayList(); list.add(Integer.valueOf(66)); int num = ((Integer) list.get(0)).intValue();
若是上面泛型代碼在 C# 實現,就不會有這麼多額外步驟。因此 Java 這種類型擦除式泛型實現方式不管使用效果與運行效率,仍是全面落後於 C# 的具現化式泛型。
因爲編譯以後,泛型就被擦除,因此在代碼運行期間,Java 虛擬機沒法獲取泛型的實際類型。
下面這段代碼,從源碼上兩個 List 看起來是不一樣類型的集合,可是通過泛型擦除以後,集合都變爲 ArrayList
。因此 if
語句中代碼將會被執行。
ArrayList<Integer> li = new ArrayList<Integer>(); ArrayList<Float> lf = new ArrayList<Float>(); if (li.getClass() == lf.getClass()) { // 泛型擦除,兩個 List 類型是同樣的 System.out.println("6666"); }
這樣代碼看起來就有點反直覺,這對新手來講不是很友好。
另外還會給咱們在實際使用中帶來一些限制,好比說咱們沒辦法直接實現如下代碼:
最後再舉個例子,好比說咱們須要實現一個泛型 List
轉換成數組的方法,咱們就沒辦法直接從 List 去獲取泛型實際類型,因此咱們不得不額外再傳入一個 Class 類型,指定數組的類型:
public static <E> E[] convert(List<E> list, Class<E> componentType) { E[] array = (E[]) Array.newInstance(componentType, list.size()); .... }
從上面的例子咱們能夠看到,Java 採用類型擦除式實現泛型,缺陷不少。那爲何 Java 不採用 C# 的那種泛型實現方式?或者說採用一種更好實現方式?
這個問題等咱們瞭解 Java 泛型機制的歷史,以及當時 Java 語言的現狀,咱們才能切身體會到當時 Java 採用這種泛型實現方式的緣由。
Java 泛型最先是在 JDK5 的時候才被引入,可是泛型思想最先來自來自 C++ 模板(template)。1996 年 Martin Odersky(Scala 語言締造者) 在剛發佈的 Java 的基礎上擴展了泛型、函數式編程等功能,造成一門新的語言-「Pizza」。
後來,Java 核心開發團隊對 Pizza 的泛型設計深感興趣,與 Martin 合做,一塊兒合做開發的一個新的項目「Generic Java」。這個項目的目的是爲了給 Java 增長泛型支持,可是不引入函數式編程等功能。最終成功在 Java5 中正式引入泛型支持。
泛型移植過程,一開始並非朝着類型擦除的方向前進,事實 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 以前編譯的程序能在新版本中正常運行,這就意味着之前沒有的限制,就不能忽然增長。
舉個例子:
ArrayList arrayList=new ArrayList(); arrayList.add("6666"); arrayList.add(Integer.valueOf(666));
沒有泛型以前, List 集合是能夠存儲不一樣類型的數據,那麼引入泛型以後,這段代碼必須的能正確運行。
爲了保證這些舊的 Clas 文件能在 Java5 以後正常運行,設計者基本有兩條路:
若是 Java 採用第一條路實現方式,那麼如今咱們可能就會有兩套集合類型。以 ArrayList
爲例,一套爲普通的 java.util.ArrayList
,一套可能爲 java.util.generic.ArrayList<T>
。
採用這種方案以後,若是開發中須要使用泛型特性,那麼直接使用新的類型。另外舊的代碼不改動,也能夠直接運行在新版本 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 核心開發人員確定會解決泛型現有的缺陷,讓咱們拭目以待。
本文是在看了『深刻 Java虛擬機(第三版)』以後,知道 Java 泛型這些故事,纔有本篇文章。
首先感謝一下機械工業出版社的小哥哥的贈書。
剛開始知道『深刻 Java虛擬機(第三版)』發佈的時候,原本覺得只是對第二版稍微補充而已。等收到這本書的時候,才發現本身錯了。兩本書放在一塊兒,徹底就不是一個量級的。
ps:盜取一張 Why 神的圖
第三本在第二版的基礎增長大量補充,也解決了第二版留下一些沒解釋的問題。因此沒買的同窗,推薦直接購買第三版。
兩個版本的具體區別,你們能夠看下 Why 神的的文章,這篇文章還被本書的做者打賞過哦。
我是樓下小黑哥,一個還未禿的程序猿,咱們下週三見~
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客: studyidea.cn