Java的"偽泛型"變"真泛型"后對(duì)性能的影響
泛型存在于Java源代碼中,在編譯為字節(jié)碼文件之前都會(huì)進(jìn)行泛型擦除(type erasure),因此,Java的泛型完全由Javac等編譯器在編譯期提供支持,可以理解為Java的一顆語法糖,這種方式實(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á)到了方便使用的目的,例如在容器的使用過程中,能夠記住其類型,這樣就不用在獲取時(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)行封裝,不過其實(shí)現(xiàn)和之前比起來要簡(jiǎn)潔,不但實(shí)例字段內(nèi)存占用減少,還沒有了裝箱和拆箱操作,也省去了強(qiáng)制類型轉(zhuǎn)換。這也是我們?cè)谑褂肑ava泛型時(shí)希望看到的版本。
以目前Java的實(shí)現(xiàn)來看,是對(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)用中,無論是Wrapper還是Wrapper這些類型來說,其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); } }
這次類型非常精確,也沒有了強(qiáng)制類型轉(zhuǎn)換。不過與我們理想中的還有差距,主要是沒有將Wrapper中的類型聲明為基本類型int,這會(huì)導(dǎo)致裝箱和拆箱操作。在Java中,裝箱和拆箱操作很頻繁,為此JDK開發(fā)人員也在努力優(yōu)化,如延遲裝箱等,現(xiàn)在,Project Valhalla要讓泛型能支持原始類型。好在除非是專門做優(yōu)化的人,否則一般開發(fā)者也不會(huì)注意到這種裝箱和拆箱的開銷。
這里還要說的是,由于對(duì)象和基本類型找不到一個(gè)共同的父類,所以在泛型擦除時(shí)只能是對(duì)象類型,也就是說,我們不能在Java中寫一個(gè)類似Wrapper這樣的聲明。這篇文章我們暫時(shí)不討論這個(gè)問題,我們討論另外一個(gè)重要的問題,也就是為任何一個(gè)泛型生成一個(gè)真正的類與這種偽泛型的擦除之間會(huì)造成哪些性能影響?
2、性能影響
我們舉幾個(gè)小例子來看一下:
實(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)并沒有覆寫這個(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()方法超過一定次數(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)分派,通過泛型擦除之后,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編譯的版本出來如下:
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)分派,通過查找方法表來實(shí)現(xiàn)調(diào)用,也就是invokevirtual invoke的意思 }
C2實(shí)際是通過運(yùn)行時(shí)采樣,發(fā)現(xiàn)t的類型只有Sub1,所以做了這樣的優(yōu)化,將動(dòng)態(tài)分派優(yōu)化為了一次比較+一次直接調(diào)用的開銷?! ?/p>
假設(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編譯出來的版本如下:
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)出來差異,更何況現(xiàn)在的C2優(yōu)化實(shí)在強(qiáng)大,使得它們的性能差異可能只在極少數(shù)的情況下才能體現(xiàn)出來。
C2編譯器非常喜歡精確類型,這樣在類型傳播的過程中能觸發(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ì)于偽泛型來說,擦除后,T是Parent類型。方法test(Parent t)無法做任何優(yōu)化。
對(duì)于真泛型來說,至少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)過類層次分析后發(fā)現(xiàn),Sub2沒有具體的實(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類可以用來進(jìn)行各種分?jǐn)?shù)運(yùn)算,需要的朋友可以參考下2015-09-09Java8 將一個(gè)List<T>轉(zhuǎn)為Map<String,T>的操作
這篇文章主要介紹了Java8 將一個(gè)List<T>轉(zhuǎn)為Map<String, T>的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02數(shù)組在java中的擴(kuò)容的實(shí)例方法
在本篇文章里小編給大家分享的是一篇關(guān)于數(shù)組在java中的擴(kuò)容的實(shí)例方法內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-01-01SpringBoot接口加密與解密的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot接口加密與解密的實(shí)現(xiàn)2023-10-10