溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java泛型的實現(xiàn)方式是什么

發(fā)布時間:2021-11-02 10:59:42 來源:億速云 閱讀:389 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“Java泛型的實現(xiàn)方式是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“Java泛型的實現(xiàn)方式是什么”吧!

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

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

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

Java泛型的實現(xiàn)方式是什么

方法源碼

Java泛型的實現(xiàn)方式是什么


字節(jié)碼

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

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

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

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

除此之外,編譯器還會對泛型安全性防御,如果我們往 ArrayList添加 Integer,程序編譯期間就會報錯。

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

Java泛型的實現(xiàn)方式是什么

類型擦除帶來的缺陷

作為對比,我們再來簡單聊下 C# 泛型的實現(xiàn)方式。

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

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

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

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

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

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

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

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

運行效率

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

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

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

所以對于下面這段代碼來說:

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

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

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

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

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

運行期間無法獲取泛型實際類型

由于編譯之后,泛型就被擦除,所以在代碼運行期間,Java 虛擬機無法獲取泛型的實際類型。

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

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

這樣代碼看起來就有點反直覺,這對新手來說不是很友好。

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

Java泛型的實現(xiàn)方式是什么

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

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

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

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

Java 泛型歷史背景

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

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

Java泛型的實現(xiàn)方式是什么

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

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

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

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

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

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

舉個例子:

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

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

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

  1. 需要泛型化的容器(主要是容器類型),以前有的保持不變,平行增加一套新的泛型化的版本。

  2. 直接把已有的類型原地泛型化,不增加任何新的已有類型的泛型版本。

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

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

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

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

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

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

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

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

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

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

到此,相信大家對“Java泛型的實現(xiàn)方式是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI