主頁 > 後端開發 > 用了這么多年的 Java 泛型,你對它到底有多了解?

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

2020-10-22 22:47:13 後端開發

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/186220.html

標籤:Java

上一篇:如何安放你的大檔案,MongoDB GridFS可以幫助你

下一篇:java.lang.String 的 + 號操作到底做了什么?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more