Java的"偽泛型"變"真泛型"后對(duì)性能的影響
泛型存在于Java源代碼中,在編譯為字節(jié)碼文件之前都會(huì)進(jìn)行泛型擦除(type erasure),因此,Java的泛型完全由Javac等編譯器在編譯期提供支持,可以理解為Java的一顆語(yǔ)法糖,這種方式實(shí)現(xiàn)的泛型有時(shí)也稱為“偽泛型”。
泛型擦除本質(zhì)上就是擦除與泛型相關(guān)的一切信息,例如參數(shù)化類型、類型變量等,Javac還將在需要時(shí)進(jìn)行類型檢查及強(qiáng)制類型轉(zhuǎn)換,甚至在必要時(shí)會(huì)合成橋方法。
1、真假泛型
如果你是Java業(yè)務(wù)開發(fā)者,其實(shí)這種所謂的偽泛型已經(jīng)達(dá)到了方便使用的目的,例如在容器的使用過(guò)程中,能夠記住其類型,這樣就不用在獲取時(shí)專門做強(qiáng)制類型轉(zhuǎn)換了,如下:
package cn.hotspotvm;
class Wrapper<T> {
public T data;
public void setData(T data) {
this.data = data;
}
public T getData(){
return data;
}
}
public class TestGeneric {
public static void main(String[] args) {
Wrapper<Integer> p = new Wrapper<>();
p.setData(new Integer(2));
// 在獲取的時(shí)候不用進(jìn)行強(qiáng)制類型轉(zhuǎn)換,直接用
// Integer接收即可
Integer data = p.getData();
System.out.println(data);
}
}泛型尤其在我們使用容器類,如ArrayList、 HashMap等時(shí)能提供便利。
假設(shè)Java不支持泛型,那么我們針對(duì)Integer類型進(jìn)行封裝的Wrapper代碼應(yīng)該是如下的樣子:
class Wrapper{
public int data;
public void setData(int data) {
this.data = data;
}
public Object getData(){
return data;
}
}
public class TestGeneric {
public static void main(String[] args) {
Wrapper p = new Wrapper();
p.setData(2);
int data = p.getData();
System.out.println(data);
}
}這個(gè)Wrapper明顯是只能對(duì)int類型進(jìn)行封裝,不過(guò)其實(shí)現(xiàn)和之前比起來(lái)要簡(jiǎn)潔,不但實(shí)例字段內(nèi)存占用減少,還沒(méi)有了裝箱和拆箱操作,也省去了強(qiáng)制類型轉(zhuǎn)換。這也是我們?cè)谑褂肑ava泛型時(shí)希望看到的版本。
以目前Java的實(shí)現(xiàn)來(lái)看,是對(duì)泛型進(jìn)行擦除處理的,對(duì)Wrapper類型進(jìn)行擦除后的代碼如下所示。
class Wrapper{
public Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData(){
return data;
}
}
public class TestGeneric {
public static void main(String[] args) {
Wrapper p = new Wrapper();
p.setData(2);
// 必須進(jìn)行強(qiáng)制類型轉(zhuǎn)換為Integer
Integer data = (Integer)p.getData();
System.out.println(data);
}
}由于在整個(gè)應(yīng)用中,無(wú)論是Wrapper還是Wrapper這些類型來(lái)說(shuō),其Wrapper在虛擬機(jī)中只有一個(gè)版本,因?yàn)樾枰獙?duì)任何的Java對(duì)象進(jìn)行封裝,所以聲明為Object,當(dāng)然如果你知道只會(huì)放某個(gè)更具體的類或這個(gè)類的子類時(shí),也可以將Wrapper類型聲明的更精確一些,如Wrapper。
假設(shè)像C++的模板類一樣,Java的“真泛型”也為每一個(gè)具體的泛型生成一個(gè)獨(dú)有的類(類型膨脹式泛型),那么就如下圖所示。

可以看到,真泛型會(huì)針對(duì)具體的類型生成獨(dú)有的類型。針對(duì)Wrapper就應(yīng)該有是這樣:
class Wrapper{
public Integer data;
public void setData(Integer data) {
this.data = data;
}
public Integer getData(){
return data;
}
}
public class TestGeneric {
public static void main(String[] args) {
Wrapper p = new Wrapper();
p.setData(2);
Integer data = p.getData();
System.out.println(data);
}
}這次類型非常精確,也沒(méi)有了強(qiáng)制類型轉(zhuǎn)換。不過(guò)與我們理想中的還有差距,主要是沒(méi)有將Wrapper中的類型聲明為基本類型int,這會(huì)導(dǎo)致裝箱和拆箱操作。在Java中,裝箱和拆箱操作很頻繁,為此JDK開發(fā)人員也在努力優(yōu)化,如延遲裝箱等,現(xiàn)在,Project Valhalla要讓泛型能支持原始類型。好在除非是專門做優(yōu)化的人,否則一般開發(fā)者也不會(huì)注意到這種裝箱和拆箱的開銷。
這里還要說(shuō)的是,由于對(duì)象和基本類型找不到一個(gè)共同的父類,所以在泛型擦除時(shí)只能是對(duì)象類型,也就是說(shuō),我們不能在Java中寫一個(gè)類似Wrapper這樣的聲明。這篇文章我們暫時(shí)不討論這個(gè)問(wèn)題,我們討論另外一個(gè)重要的問(wèn)題,也就是為任何一個(gè)泛型生成一個(gè)真正的類與這種偽泛型的擦除之間會(huì)造成哪些性能影響?
2、性能影響
我們舉幾個(gè)小例子來(lái)看一下:
實(shí)例1
class SpecWrapper extends Wrapper<Integer> {
public void setData(Integer data) { }
}我們自定義了一個(gè)對(duì)Integer類封裝的SpecWrapper類,這個(gè)類在泛型擦除后如下所示。
class SpecWrapper extends Wrapper {
public void setData(Integer data) { }
/*synthetic*/ public void setData(Object x0) { // 合成的橋方法
this.setData((Integer)x0);
}
}在泛型擦除后,Wrapper類的setData()方法的類型變量T被替換為Object類型,這樣SpecWrapper類中的setData(Integer data)并沒(méi)有覆寫這個(gè)方法,所以為了覆寫特性,向SpecWrapper類中添加一個(gè)合成的橋方法setData(Objext x0)。
這會(huì)讓我們?cè)趯?shí)際調(diào)用setData()方法時(shí)調(diào)用的是setData(Object x0)方法,這個(gè)方法又調(diào)用了setData(Integer data)方法,而在“真泛型”中,我們直接調(diào)用setData(Integer data)方法即可,雖然JIT會(huì)大概率對(duì)這種簡(jiǎn)單的方法進(jìn)行內(nèi)聯(lián),但是性能影響肯定是有的。
實(shí)例2
class Parent {
public static int a = 0;
public void invoke() {
a = 1;
}
}
final class Sub1 extends Parent {
public void invoke() {
a = 2;
}
}
public class TestGeneric<T extends Parent> {
T t;
public TestGeneric(T t1) {
this.t = t1;
}
public static void main(String[] args)
throws InterruptedException {
TestGeneric<Sub1> s
= new TestGeneric<>(new Sub1());
// 調(diào)用test()方法超過(guò)一定次數(shù)會(huì)分別觸發(fā)C1和C2編譯
for (int i = 0; i < 100000; i++) {
s.test();
}
// 等待異常的編譯線程編譯完并打印結(jié)果
Thread.sleep(10000);
}
public void test() {
t.invoke();
}
}我們關(guān)注一下test()方法中的t.invoke()動(dòng)態(tài)分派,通過(guò)泛型擦除之后,TestGeneric類中的T是Parent類型,那么test()方法中t聲明的類型就是Parent,如下:
public class TestGeneric{
Parent t;
public TestGeneric(Parent t1) {
this.t = t1;
}
public void test() {
t.invoke();
}
}配置如下命令查看C2編譯的結(jié)果:
XX:CompileCommand=compileonly,cn/hotspotvm/TestGeneric::test -XX:CompileCommand=print,cn/hotspotvm/TestGeneric::test
C2編譯的版本出來(lái)如下:
0x000070e6e00aa915: cmp $0xf800c184,%r8d ; {metadata('cn/hotspotvm/Sub1')}
0x000070e6e00aa91c: jne 0x000070e6e00aa934
0x000070e6e00aa91e: lea (%r12,%r10,8),%rsi
0x000070e6e00aa922: nop
0x000070e6e00aa923: callq 0x000070e6dfe45e60 ; OopMap{off=72}
;*invokevirtual invoke
; - cn.hotspotvm.TestGeneric::test@4 (line 39)
; {optimized virtual_call}
0x000070e6e00aa928: add $0x10,%rsp
0x000070e6e00aa92c: pop %rbp
0x000070e6e00aa92d: test %eax,0x165cb6cd(%rip) # 0x000070e6f6676000
; {poll_return}
0x000070e6e00aa933: retq
0x000070e6e00aa934: mov $0xffffffde,%esi
0x000070e6e00aa939: mov %r10d,%ebp
0x000070e6e00aa93c: data32 xchg %ax,%ax
0x000070e6e00aa93f: callq 0x000070e6dfe45460 ; OopMap{rbp=NarrowOop off=100}
;*invokevirtual invoke
; - cn.hotspotvm.TestGeneric::test@4 (line 39)
; {runtime_call}也就等同于如下:
if(t是Sub1類型){
直接調(diào)用Sub1的invoke()方法,也就是optimized virtual_call的意思
}else{
動(dòng)態(tài)分派,通過(guò)查找方法表來(lái)實(shí)現(xiàn)調(diào)用,也就是invokevirtual invoke的意思
}C2實(shí)際是通過(guò)運(yùn)行時(shí)采樣,發(fā)現(xiàn)t的類型只有Sub1,所以做了這樣的優(yōu)化,將動(dòng)態(tài)分派優(yōu)化為了一次比較+一次直接調(diào)用的開銷。
假設(shè)是“真泛型”上場(chǎng),那TestGeneric應(yīng)該是如下的樣子:
public class TestGeneric{
Sub1 t;
public TestGeneric(Sub1 t1) {
this.t = t1;
}
public void test() {
t.invoke();
}
}此時(shí)C2編譯出來(lái)的版本如下:
0x000074bd840b7b5b: callq 0x000074bd83e45e60 ; OopMap{off=64}
;*invokevirtual invoke
; - cn.hotspotvm.TestGeneric::test@4 (line 39)
; {optimized virtual_call}直接就是直調(diào),比之前省了一次判斷,代碼很簡(jiǎn)潔。因?yàn)镃2得到了t的更精確類型Sub1,并且這個(gè)Sub1還是final修飾的類,不會(huì)有子類,所以直接調(diào)用即可。
雖然少量調(diào)用可能并不能體現(xiàn)出來(lái)差異,更何況現(xiàn)在的C2優(yōu)化實(shí)在強(qiáng)大,使得它們的性能差異可能只在極少數(shù)的情況下才能體現(xiàn)出來(lái)。
C2編譯器非常喜歡精確類型,這樣在類型傳播的過(guò)程中能觸發(fā)許多優(yōu)化,編譯出性能更好的版本,后續(xù)我們?cè)诮榻BC2時(shí),看一看其類型傳播,就能深刻體會(huì)到它的強(qiáng)大?!?/p>
實(shí)例3
假設(shè)有這么一個(gè)方法,實(shí)現(xiàn)如下:
class Parent{
// ...
}
find class Sub1 extends Parent{
// ...
}
class Sub2 extends Parent{
// ...
}
public class Wrapper<T extends Parent>{
public void test(T t){
if(t instanceof Sub1){
// 執(zhí)行Sub1的邏輯
}else if( t instanceof Sub2){
// 執(zhí)行Sub2的邏輯
}else{
// 執(zhí)行其它類型的邏輯
}
}
}對(duì)于偽泛型來(lái)說(shuō),擦除后,T是Parent類型。方法test(Parent t)無(wú)法做任何優(yōu)化。
對(duì)于真泛型來(lái)說(shuō),至少Wrapper和Wrapper版本中的test()是可以優(yōu)化的。
public void test(Sub1 t)版本中只需要直接執(zhí)行Sub1的邏輯就可以,并不需要判斷,因?yàn)镾ub1是final類,所以t只能是Sub1類型,如下:
public void test(Sub1 t){
執(zhí)行Sub1的邏輯
}public void test(Sub2 t)版本中只需要直接執(zhí)行Sub2的邏輯就可以,并不需要判斷,雖然Sub2是非final類型,但是經(jīng)過(guò)類層次分析后發(fā)現(xiàn),Sub2沒(méi)有具體的實(shí)現(xiàn)子類,那這時(shí)候也能認(rèn)為這個(gè)版本只有Sub2類型。
public void test(Sub2 t){
執(zhí)行Sub2的邏輯
}到此這篇關(guān)于Java的"偽泛型"變"真泛型"后,會(huì)對(duì)性能有幫助嗎?的文章就介紹到這了,更多相關(guān)Java的"偽泛型"變"真泛型"后,會(huì)對(duì)性能有幫助嗎??jī)?nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java求素?cái)?shù)和最大公約數(shù)的簡(jiǎn)單代碼示例
這篇文章主要介紹了Java求素?cái)?shù)和最大公約數(shù)的簡(jiǎn)單代碼示例,其中作者創(chuàng)建的Fraction類可以用來(lái)進(jìn)行各種分?jǐn)?shù)運(yùn)算,需要的朋友可以參考下2015-09-09
Java8 將一個(gè)List<T>轉(zhuǎn)為Map<String,T>的操作
這篇文章主要介紹了Java8 將一個(gè)List<T>轉(zhuǎn)為Map<String, T>的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
數(shù)組在java中的擴(kuò)容的實(shí)例方法
在本篇文章里小編給大家分享的是一篇關(guān)于數(shù)組在java中的擴(kuò)容的實(shí)例方法內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-01-01
使用mybatis-plus想要修改某字段為null問(wèn)題
這篇文章主要介紹了使用mybatis-plus想要修改某字段為null問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
SpringBoot接口加密與解密的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot接口加密與解密的實(shí)現(xiàn)2023-10-10

