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

来源:https://www.cnblogs.com/goodAndyxublog/archive/2020/05/22/12934938.html
-Advertisement-
Play Games

本篇文章 idea 來自 "用了這麼多年的泛型,你對它到底有多瞭解?" ,恰好當時看了「深入 Java 虛擬機的第三版」瞭解泛型的一些歷史,感覺挺有意思的,就寫了寫 Java 版的泛型。 作為一個 Java 程式員,日常編程早就離不開泛型。泛型自從 JDK1.5 引進之後,真的非常提高生產力。一個簡 ...


本篇文章 idea 來自用了這麼多年的泛型,你對它到底有多瞭解?,恰好當時看了「深入 Java 虛擬機的第三版」瞭解泛型的一些歷史,感覺挺有意思的,就寫了寫 Java 版的泛型。

作為一個 Java 程式員,日常編程早就離不開泛型。泛型自從 JDK1.5 引進之後,真的非常提高生產力。一個簡單的泛型 T,寥寥幾行代碼, 就可以讓我們在使用過程中動態替換成任何想要的類型,再也不用實現繁瑣的類型轉換方法。

雖然我們每天都在用,但是還有很多同學可能並不瞭解其中的實現原理。今天這篇我們從以下幾點聊聊 Java 泛型:

  • Java 泛型實現方式
  • 類型擦除帶來的缺陷
  • 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(); 代碼的正確性,編譯器還得在這裡插入類型轉換。

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

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

類型擦除帶來的缺陷

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

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

簡單點來講,C#實現的泛型,無論是在程式源碼,還是在編譯之後的,甚至是運行期間都是切實存在的。

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

不支持基本數據類型

泛型參數被擦除之後,強制變成了 Object 類型。這麼做對於引用類型來說沒有什麼問題,畢竟 Object 是所有類型的父類型。但是對於 int/long 等八個基本數據類型說,這就難辦了。因為 Java 沒辦法做到int/longObject 的強制轉換。

如果要實現這種轉換,需要進行一系列改造,改動難度還不小。所以當時 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 泛型歷史背景

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 之後正常運行,設計者基本有兩條路:

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

如果 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 核心開發人員肯定會解決泛型現有的缺陷,讓我們拭目以待。

幫助資料

  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

最後(求關註,求點贊,求轉發)

本文是在看了『深入 Java虛擬機(第三版)』之後,知道 Java 泛型這些故事,才有本篇文章。

首先感謝一下機械工業出版社的小哥哥的贈書。

剛開始知道『深入 Java虛擬機(第三版)』發佈的時候,本來以為只是對第二版稍微補充而已。等收到這本書的時候,才發現自己錯了。兩本書放在一起,完全就不是一個量級的。

ps:盜取一張 Why 神的圖

第三本在第二版的基礎增加大量補充,也解決了第二版留下一些沒解釋的問題。所以沒買的同學,推薦直接購買第三版。

兩個版本的具體區別,大家可以看下 Why 神的的文章,這篇文章還被本書的作者打賞過哦。

深入 Java虛擬機兩版比較

我是樓下小黑哥,一個還未禿的程式猿,我們下周三見~

歡迎關註我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關註我的博客:studyidea.cn


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 動畫 參考閱讀: https://www.cnblogs.com/xiaoyuanqujing/articles/11670140.html 插件機制(乾貨滿滿) 參考閱讀: https://www.cnblogs.com/xiaoyuanqujing/articles/11670482.html ...
  • 當我們在服務端渲染 Vue 應用時,無論伺服器執行多少次渲染,大部分 VNode 渲染出的字元串是不變的,它們有一些來自於模板的靜態 html,另一些則來自模板動態渲染的節點(雖然在客戶端動態節點有可能會變化,但是在服務端它們是不變的)。將這兩種類型的節點提取出來,僅在服務端渲染真正動態的節點(se ...
  • 問題描述: 有這樣的一段字元串: "<p class='test' id='wise'>123 456 789<br>hello<span title='hello' style='width: 200px;height:100px;' src='//www.wisewrong.com/img/12 ...
  • 路由參數解耦 一般在組件內使用路由參數,大多數人會這樣做: export default { methods: { getParamsId() { return this.$route.params.id } } } 在組件中使用 $route 會使之與其對應路由形成高度耦合,從而使組件只能在某些特 ...
  • 一、簡介 1、項目介紹 (1)基本介紹 使用 vue 以及 element-ui 搭建一個 後臺管理系統的模板。 當然,這類模板網上有很多,可以直接下載使用。 寫這個項目的目的,純屬練手(寫的比較糙)。 【layuiAdmin 後臺管理模板:(付費)】 https://www.layui.com/a ...
  • 1、Token:token是客戶端頻繁向伺服器端請求數據,伺服器頻繁的去資料庫查詢用戶名和密碼進行對比,判斷用戶名和密碼正確與否,並作出相應的提示,在這樣的背景下,token便應運而生了。 2、使用token的目的:token的目的是為了減輕伺服器的壓力,減少頻繁的查詢資料庫。 3、在前端請求後臺的 ...
  • 今天我們來談談Web和前端開發過程中需要學習什麼?前端開發需要使用什麼開發工具?也簡單介紹前端開發前景和薪水。 前端工程師的主要職責: 前端工程師在不同的公司有不同的功能,但性質相似。 1、網站設計與網頁界面開發 2、做網站界面開發 3、Web界面開發,前端數據綁定,前臺邏輯 4、設計、開發、數據 ...
  • 【目錄】 一、jQuery操作標簽 二、jQuery綁定事件 一、jQuery操作標簽 1、操作類 class js版本 jQuery版本classList.add() addClass()classList.remove() removeClass()classList.contains() ha ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...