當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]作為一個(gè) Java 程序員,日常編程早就離不開泛型。泛型自從 JDK1.5 引進(jìn)之后,真的非常提高生產(chǎn)力。一個(gè)簡(jiǎn)單的泛型 T,寥寥幾行代碼, 就可以讓我們?cè)谑褂眠^程中動(dòng)態(tài)替換成任何想要的類型,再也不用實(shí)現(xiàn)繁瑣的類型轉(zhuǎn)換方法。 雖然我們每天都在用,但是還有很多

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

作為一個(gè) Java 程序員,日常編程早就離不開泛型。泛型自從 JDK1.5 引進(jìn)之后,真的非常提高生產(chǎn)力。一個(gè)簡(jiǎn)單的泛型 T,寥寥幾行代碼, 就可以讓我們?cè)谑褂眠^程中動(dòng)態(tài)替換成任何想要的類型,再也不用實(shí)現(xiàn)繁瑣的類型轉(zhuǎn)換方法。

雖然我們每天都在用,但是還有很多同學(xué)可能并不了解其中的實(shí)現(xiàn)原理。今天這篇我們從以下幾點(diǎn)聊聊 Java 泛型:

  • Java 泛型實(shí)現(xiàn)方式
  • 類型擦除帶來的缺陷
  • Java 泛型發(fā)展史
用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

Java 泛型實(shí)現(xiàn)方式

Java 采用**類型擦除(Type erasure generics)**的方式實(shí)現(xiàn)泛型。用大白話講就是這個(gè)泛型只存在源碼中,編譯器將源碼編譯成字節(jié)碼之時(shí),就會(huì)把泛型『擦除』,所以字節(jié)碼中并不存在泛型。

對(duì)于下面這段代碼,編譯之后,我們使用 javap -s class 查看字節(jié)碼。

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)
方法源碼
用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)
字節(jié)碼

觀察setParam 部分的字節(jié)碼,從 descriptor 可以看到,泛型 T 已被擦除,最終替換成了 Object

ps:并不是每一個(gè)泛型參數(shù)被擦除類型后都會(huì)變成 Object 類,如果泛型類型為  T extends String 這種方式,最終泛型擦除之后將會(huì)變成 String。

同理getParam 方法,泛型返回值也被替換成了 Object。

為了保證 String param = genericType.getParam(); 代碼的正確性,編譯器還得在這里插入類型轉(zhuǎn)換。

除此之外,編譯器還會(huì)對(duì)泛型安全性防御,如果我們往 ArrayList<String> 添加 Integer,程序編譯期間就會(huì)報(bào)錯(cuò)。

最終類型擦除后的代碼等同與如下:

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

類型擦除帶來的缺陷

作為對(duì)比,我們?cè)賮砗?jiǎn)單聊下 C# 泛型的實(shí)現(xiàn)方式。

**C#**泛型實(shí)現(xiàn)方式為「具現(xiàn)化式泛型(Reifiable generics)」,不熟悉的 C#小伙伴可以不用糾結(jié)具現(xiàn)化技術(shù)概念,我也不了解這些特性--!

簡(jiǎn)單點(diǎn)來講,**C#**實(shí)現(xiàn)的泛型,無論是在程序源碼,還是在編譯之后的,甚至是運(yùn)行期間都是切實(shí)存在的。

相對(duì)比與 C# 泛型,Java 泛型看起來就像是個(gè)「」泛型。Java 泛型只存在程序源碼中,編譯之后就被擦除,這種缺陷相應(yīng)的會(huì)帶來一些問題。

不支持基本數(shù)據(jù)類型

泛型參數(shù)被擦除之后,強(qiáng)制變成了 Object 類型。這么做對(duì)于引用類型來說沒有什么問題,畢竟 Object 是所有類型的父類型。但是對(duì)于 int/long 等八個(gè)基本數(shù)據(jù)類型說,這就難辦了。因?yàn)?Java 沒辦法做到int/longObject 的強(qiáng)制轉(zhuǎn)換。

如果要實(shí)現(xiàn)這種轉(zhuǎn)換,需要進(jìn)行一系列改造,改動(dòng)難度還不小。所以當(dāng)時(shí) Java 給出一個(gè)簡(jiǎn)單粗暴的解決方案:既然沒辦法做到轉(zhuǎn)換,那就索性不支持原始類型泛型了。

如果需要使用,那就規(guī)定使用相關(guān)包裝類的泛型,比如 ArrayList<Integer>。另外為了開發(fā)人員方便,順便增加了原生數(shù)據(jù)類型的自動(dòng)拆箱/裝箱的特性。

正是這種「偷懶」的做法,導(dǎo)致現(xiàn)在我們沒辦法使用原始類型泛型,又要忍受包裝類裝箱/拆箱帶來的開銷,從而又帶來運(yùn)行效率的問題。

運(yùn)行效率

上面字節(jié)碼例子我們已經(jīng)看到,泛型擦除之后類型將會(huì)變成 Object。當(dāng)泛型出現(xiàn)在方法輸入位置的時(shí)候,由于 Java 是可以向上轉(zhuǎn)型的,這里并不需要強(qiáng)制類型轉(zhuǎn)換,所以沒有什么問題。

但是當(dāng)泛型參數(shù)出現(xiàn)在方法的輸出位置(返回值)的時(shí)候,調(diào)用該方法的地方就需要進(jìn)行向下轉(zhuǎn)換,將 Object 強(qiáng)制轉(zhuǎn)換成所需類型,所以編譯器會(huì)插入一句 checkcast 字節(jié)碼。

除了這個(gè),上面我們還說到原始基本數(shù)據(jù)類型,編譯器還需幫助我們進(jìn)行裝箱/拆箱。

所以對(duì)于下面這段代碼來說:

List<Integer> list = new ArrayList<Integer>();
list.add(66); // 1
int num = list.get(0); // 2

對(duì)于①處,編譯器要做就是增加基本類型的裝箱即可。但是對(duì)于第二步來說,編譯器首先需要將 Object 強(qiáng)制轉(zhuǎn)換成 Integer,接著編譯器還需要進(jìn)行拆箱。

類型擦除之后,上面代碼等同于:

List list = new ArrayList();
list.add(Integer.valueOf(66));
int num = ((Integer) list.get(0)).intValue();

如果上面泛型代碼在 C# 實(shí)現(xiàn),就不會(huì)有這么多額外步驟。所以 Java 這種類型擦除式泛型實(shí)現(xiàn)方式無論使用效果與運(yùn)行效率,還是全面落后于 C# 的具現(xiàn)化式泛型。

運(yùn)行期間無法獲取泛型實(shí)際類型

由于編譯之后,泛型就被擦除,所以在代碼運(yùn)行期間,Java 虛擬機(jī)無法獲取泛型的實(shí)際類型。

下面這段代碼,從源碼上兩個(gè) List 看起來是不同類型的集合,但是經(jīng)過泛型擦除之后,集合都變?yōu)?ArrayList。所以 if語(yǔ)句中代碼將會(huì)被執(zhí)行。

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // 泛型擦除,兩個(gè) List 類型是一樣的
    System.out.println("6666");
}

這樣代碼看起來就有點(diǎn)反直覺,這對(duì)新手來說不是很友好。

另外還會(huì)給我們?cè)趯?shí)際使用中帶來一些限制,比如說我們沒辦法直接實(shí)現(xiàn)以下代碼:

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

最后再舉個(gè)例子,比如說我們需要實(shí)現(xiàn)一個(gè)泛型 List 轉(zhuǎn)換成數(shù)組的方法,我們就沒辦法直接從 List 去獲取泛型實(shí)際類型,所以我們不得不額外再傳入一個(gè) Class 類型,指定數(shù)組的類型:

public static <E> E[] convert(List<E> list, Class<E> componentType) {
    E[] array = (E[]) Array.newInstance(componentType, list.size());
    ....
}

從上面的例子我們可以看到,Java 采用類型擦除式實(shí)現(xiàn)泛型,缺陷很多。那為什么 Java 不采用 C# 的那種泛型實(shí)現(xiàn)方式?或者說采用一種更好實(shí)現(xiàn)方式?

這個(gè)問題等我們了解 Java 泛型機(jī)制的歷史,以及當(dāng)時(shí) Java 語(yǔ)言的現(xiàn)狀,我們才能切身體會(huì)到當(dāng)時(shí) Java 采用這種泛型實(shí)現(xiàn)方式的原因。

Java 泛型歷史背景

Java 泛型最早是在 JDK5 的時(shí)候才被引入,但是泛型思想最早來自來自 C++ 模板(template)。1996 年 Martin Odersky(Scala 語(yǔ)言締造者) 在剛發(fā)布的 Java 的基礎(chǔ)上擴(kuò)展了泛型、函數(shù)式編程等功能,形成一門新的語(yǔ)言-「Pizza」。

后來,Java 核心開發(fā)團(tuán)隊(duì)對(duì) Pizza 的泛型設(shè)計(jì)深感興趣,與 Martin 合作,一起合作開發(fā)的一個(gè)新的項(xiàng)目「Generic Java」。這個(gè)項(xiàng)目的目的是為了給 Java 增加泛型支持,但是不引入函數(shù)式編程等功能。最終成功在 Java5 中正式引入泛型支持。

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

泛型移植過程,一開始并不是朝著類型擦除的方向前進(jìn),事實(shí) Pizza 中泛型更加類似于 C# 中的泛型。

但是由于 Java 自身特性,自帶嚴(yán)格的約束,讓 Martin 在Generic Java 開發(fā)過程中,不得不放棄了 Pizza 中泛型設(shè)計(jì)。

這個(gè)特性就是,Java 需要做到嚴(yán)格的向后兼容性。也就是說一個(gè)在 JDK1.2 編譯出來 Class 文件,不僅能在 JDK 1.2 能正常運(yùn)行,還得必須保證在后續(xù) JDK,比如 JDK12 中也能保證正常的運(yùn)行。

這種特性是明確寫入 Java 語(yǔ)言規(guī)范的,這是一個(gè)對(duì) Java 使用者的一個(gè)嚴(yán)肅承諾。

這里強(qiáng)調(diào)一下,這里的向后兼容性指的是二進(jìn)制兼容性,并不是源碼兼容性。也不保證高版本的 Class 文件能夠運(yùn)行在低版本的 JDK 上。

現(xiàn)在困難點(diǎn)在于,Java 1.4.2 之前都沒有支持泛型,而 Java5 之后突然要支持泛型,還要讓 JDK1.4 之前編譯的程序能在新版本中正常運(yùn)行,這就意味著以前沒有的限制,就不能突然增加。

舉個(gè)例子:

ArrayList arrayList=new ArrayList();
arrayList.add("6666");
arrayList.add(Integer.valueOf(666));

沒有泛型之前, List 集合是可以存儲(chǔ)不同類型的數(shù)據(jù),那么引入泛型之后,這段代碼必須的能正確運(yùn)行。

為了保證這些舊的 Clas 文件能在 Java5 之后正常運(yùn)行,設(shè)計(jì)者基本有兩條路:

  1. 需要泛型化的容器(主要是容器類型),以前有的保持不變,平行增加一套新的泛型化的版本。
  2. 直接把已有的類型原地泛型化,不增加任何新的已有類型的泛型版本。

如果 Java 采用第一條路實(shí)現(xiàn)方式,那么現(xiàn)在我們可能就會(huì)有兩套集合類型。以 ArrayList 為例,一套為普通的 java.util.ArrayList,一套可能為 java.util.generic.ArrayList<T>。

采用這種方案之后,如果開發(fā)中需要使用泛型特性,那么直接使用新的類型。另外舊的代碼不改動(dòng),也可以直接運(yùn)行在新版本 JDK 中。

這套方案看起來沒什么問題,實(shí)際上C# 就是采用這套方案。但是為什么 Java  卻沒有使用這套方案那?

這是因?yàn)楫?dāng)時(shí) C# 才發(fā)布兩年,歷史代碼并不多,如果舊代碼需要使用泛型特性,改造起來也很快。但是 Java 不一樣,當(dāng)時(shí) Java 已經(jīng)發(fā)布十年了,已經(jīng)有很多程序已經(jīng)運(yùn)行部署在生產(chǎn)環(huán)境,可以想象歷史代碼非常多。

如果這些應(yīng)用在新版本 Java 需要使用泛型,那就需要做大量源碼改動(dòng),可以想象這個(gè)開發(fā)工作量。

另外 Java 5   之前,其實(shí)我們就已經(jīng)有了兩套集合容器,一套為 Vector/Hashtable 等容器,一套為 ArrayList/ HashMap。這兩套容器的存在,其實(shí)已經(jīng)引來一些不便,對(duì)于新接觸的 Java 的開發(fā)人員來說,還得學(xué)習(xí)這兩者的區(qū)別。

如果此時(shí)為了泛型再引入新類型,那么就會(huì)有四套容器同時(shí)并存。想想這個(gè)畫面,一個(gè)新接觸開發(fā)人員,面對(duì)四套容器,完全不知道如何下手選擇。如何 Java 真的這么實(shí)現(xiàn)了,想必會(huì)有更多人吐槽 Java。

所以 Java 選擇第二條路,采用類型擦除,只需要改動(dòng) Javac 編譯器,不需要改動(dòng)字節(jié)碼,不需要改動(dòng)虛擬機(jī),也保證了之前歷史沒有泛型的代碼還可以在新的 JDK 中運(yùn)行。

但是第二條路,并不代表一定需要使用類型擦除實(shí)現(xiàn),如果有足夠時(shí)間好好設(shè)計(jì),也許會(huì)有更好的方案。

當(dāng)年留下的技術(shù)債,現(xiàn)在只能靠 Valhalla 項(xiàng)目來還了。這個(gè)項(xiàng)目從2014 年開始立項(xiàng),原本計(jì)劃在 JDK10 中解決現(xiàn)有語(yǔ)言的各種缺陷。但是結(jié)果我們也知道了,現(xiàn)在都 JDK14 了,還只是完成少部分目標(biāo),并沒有解決核心目標(biāo),可見這個(gè)改動(dòng)的難度啊。

總結(jié)

本文我們先從 Java 泛型底層實(shí)現(xiàn)方式開始聊起,接著舉了幾個(gè)例子,讓大家了解現(xiàn)在泛型實(shí)現(xiàn)方式存在一些缺陷。

然后我們帶入 Java 泛型歷史背景,站在 Java 核心開發(fā)者的角度,才能了解 Java 泛型這么現(xiàn)實(shí)無奈原因。

最后作為 Java 開發(fā)者,讓我們對(duì)于現(xiàn)在  Java 一些不足,少些抱怨,多一些理解吧。相信之后 Java 核心開發(fā)人員肯定會(huì)解決泛型現(xiàn)有的缺陷,讓我們拭目以待。

幫助資料

  1. https://www.zhihu.com/question/38940308
  2. https://www.zhihu.com/question/28665443
  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

特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

長(zhǎng)按訂閱更多精彩▼

用了這么多年的 Java 泛型,你對(duì)它到底有多了解?|原創(chuàng)

如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉