你,可能沒完全搞懂 Java 泛型
本文轉(zhuǎn)載自微信公眾號「yes的練級攻略」,作者是yes呀。轉(zhuǎn)載本文請聯(lián)系yes的練級攻略公眾號。
大家好,我是yes。
今天我們來談?wù)劮盒?。其?shí)在初學(xué)的時(shí)候,我就對泛型有點(diǎn)蒙,因?yàn)榭吹接腥苏f Java 的泛型不是真的泛型,我搞不懂。
還有人說 Java 的泛型在實(shí)際運(yùn)行時(shí)候會把類型給擦除了,我想著擦除是什么意思?為什么要擦除?
那把類型給擦除了為什么反射的時(shí)候還能得到泛型的類型信息?
我們今天就來盤一盤泛型:
- 為什么需要泛型?
- 為什么都說Java的泛型是偽泛型?
- 為什么Java泛型的實(shí)現(xiàn)是類型擦除?
- 既然擦除了類型,為什么在運(yùn)行期仍能反射獲得類型?
話不多說,發(fā)車!
為什么需要泛型
我們都知道在 Java5 之前是沒有泛型的,沒泛型都能用的好好的,那為什么要加個(gè)泛型呢,能給我們帶來什么呢?
我們先來看下下面這段代碼:
- List list = new ArrayList();
- list.add("yes"); // 加入string
- list.add(233); // 加入int
在沒有泛型的時(shí)候,加入的集合的數(shù)據(jù)并不會做任何約束,都會被當(dāng)作成 Object 類型。
可能有人說,這很好呀,多自由!確實(shí),自由是自由了,但是代碼的約束能力越低,就越容易出錯(cuò),使用上也有諸多不便,比如獲取的時(shí)候需要強(qiáng)轉(zhuǎn)。
如果一不小心取錯(cuò)類型,編譯的時(shí)候能過,但是運(yùn)行的時(shí)候卻拋錯(cuò)。
綜上,Java 引入了泛型。
而泛型的作用就是加了一層約束,約束了類型。
有了這一層約束就好辦事兒了,由于聲明了類型,可以在編譯的時(shí)候就識別出不準(zhǔn)確的類型元素。使得錯(cuò)誤提早拋出,避免運(yùn)行時(shí)才發(fā)現(xiàn)。
并且也不需要在代碼上顯示的強(qiáng)轉(zhuǎn),從以下代碼可以看出,能直接獲取 String 類型元素。
我們再小結(jié)一下泛型的好處:
提高了代碼的可讀性,一眼就能看出集合(其它泛型類)的類型
可在編譯期檢查類型安全,增加程序的健壯性
省心不需要強(qiáng)轉(zhuǎn)(其實(shí)內(nèi)部幫做了強(qiáng)轉(zhuǎn),下面會說)
提高代碼的復(fù)用率,定義好泛型,一個(gè)方法(類)可以適配所有類型 (其實(shí)以前 Object 也行,就是比較麻煩)
為什么都說Java的泛型是偽泛型
看起來我們平日用的一些泛型好像沒啥毛病啊?為什么都說Java的泛型是偽泛型?哪里偽了?
我們再來看一段代碼:
可以看到,我聲明的是一個(gè) String 類型的集合,但是通過反射往集合中插入了 int 類型的數(shù)據(jù),居然成功了???
這說明在運(yùn)行時(shí)泛型根本沒有起作用!也就是說在運(yùn)行的時(shí)候 JVM 獲取不到泛型的信息,也會不對其做任何的約束。
你可以認(rèn)為 Java 的泛型就是編譯的時(shí)候生效,運(yùn)行的時(shí)候沒有泛型,所以大家才說 Java 是偽泛型!
因此,雖然在 IDE 寫代碼的時(shí)候泛型生效了,而實(shí)際上在運(yùn)行的時(shí)候泛型的類型是被擦除的。
一言蔽之,Java的泛型只在編譯時(shí)生效,JVM 運(yùn)行時(shí)沒有泛型。
為什么Java泛型的實(shí)現(xiàn)是類型擦除?
類型擦除 (type Erasure)。
Java 之所以在運(yùn)行時(shí)將類型擦除的原因是為了向下兼容,即兼容 Java5 之前的編譯的 class 文件。
例如 Java 1.2 上正在跑的代碼,可以在 Java 5 的 JRE 上運(yùn)行。
就是為了這該死的向下兼容,才使得 Java 實(shí)現(xiàn)的是偽泛型。
我從現(xiàn)有的實(shí)現(xiàn)倒推偽泛型的設(shè)計(jì)可能思路(我個(gè)人瞎掰的,您隨意聽聽)是這樣的:
- 這 Java 5 以前的版本,線上已經(jīng)有很多應(yīng)用在跑了,我好像不能新加一套,影響推廣還可能被罵的很慘
- 咋辦,泛型畢竟是加一個(gè)約束,以前的代碼沒這個(gè)約束啊,該如何兼容?
- 有了,要不我在編譯器上動(dòng)手腳,在編譯的時(shí)候識別和約束泛型,然后編譯過了就把泛型的信息擦除了。這樣運(yùn)行的時(shí)候約束不是沒了嗎?不就和之前保持一致了嗎?好,就這樣干了!
總而言之,就是為了向下兼容才采用類型擦除來實(shí)現(xiàn)的。
這里還有個(gè)坑,也就是泛型不支持基本類型,比如 int。因?yàn)榉盒筒脸缶妥兂闪薕bject,這個(gè) int 和 Object 兼容有點(diǎn)麻煩。
我在網(wǎng)上看 R大的解釋如下:
GJ / Java 5說:這個(gè)問題有點(diǎn)麻煩,趕不及在這個(gè)版本發(fā)布前完成了,就先放著不管吧。于是Java 5的泛型就不支持原始類型,而我們不得不寫惡心的ArrayList、ArrayList…
這就是一個(gè)偷懶了的地方。
emmm,這說明啥?寫 Java 的也是程序員,也是要發(fā)版有上線需求的,所以說......
好了,言歸正傳,現(xiàn)在 Java 的泛型實(shí)現(xiàn)確實(shí)是偽泛型??吹竭@不經(jīng)有人會發(fā)問?難道就只能一直偽泛型了嗎?
那啥,我覺得吧,只要時(shí)間允許,只要錢夠,應(yīng)該都能做?哈哈哈。
既然擦除了類型,為什么在運(yùn)行期仍能反射獲得類型?
難道是沒擦干凈?別急,我們慢慢看。
我們先來回顧一下這段代碼:
我們定義了泛型類型為 String 的 list,并且獲取的 str 不需要強(qiáng)轉(zhuǎn),這一步是怎么做的呢?我們 javap -c 看下字節(jié)碼:
我們從反編譯看生成的字節(jié)碼可以看到, new 的 list 沒有保存泛型的信息,所以是被擦除了。
然后看到 #7 沒,有個(gè) checkcast ,強(qiáng)轉(zhuǎn)的類型是 String,看到這大伙兒應(yīng)該都明白,為什么類型擦除了,但是我們 get 的時(shí)候不需要強(qiáng)轉(zhuǎn)呢?因?yàn)榫幾g器隱性的幫我們插入了強(qiáng)轉(zhuǎn)的代碼!所以我們的 Java 代碼中不需要寫強(qiáng)轉(zhuǎn)。
再回到此小節(jié)標(biāo)題:既然擦除了類型,為什么在運(yùn)行期仍能反射獲得類型?
答案就藏在 class 文件中。我們來看下這段代碼:
通過反射,我確實(shí)獲得了 list 的類型。那既然類型被擦除了,這又是怎么做到的呢?
我們直接進(jìn)行一手 javap -v,反編譯看到字節(jié)碼里面有這樣的記錄:
這下很好理解了,class 文件里面存了這個(gè)信息,所以我們通過反射自然而然的就能得到這個(gè)類型。沒錯(cuò),就是這么簡單。
也正因?yàn)樵砣绱?,所以我們只能對以下三種情況利用反射獲取泛型類型:
- 成員變量的泛型
- 方法入?yún)⒌姆盒?/li>
- 方法返回值的泛型
對于局部變量這種是無能為力的。
最后
好了,今天關(guān)于泛型的文章暫時(shí)先到這,其實(shí)泛型的東西還沒講完,比如通配符、上界下界的限制(泛型的 PECS 原則),再如泛型的橋接,以及橋接的坑。
東西還挺多的,所以放下篇!等著哈。
參考
https://www.zhihu.com/question/28665443/answer/118148143