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)和之前比起來要簡潔,不但實(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ì)這種簡單的方法進(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),比之前省了一次判斷,代碼很簡潔。因?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)大。
實(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ì)性能有幫助嗎?內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java web實(shí)現(xiàn)賬號(hào)單一登錄,防止同一賬號(hào)重復(fù)登錄(踢人效果)
這篇文章主要介紹了Java web實(shí)現(xiàn)賬號(hào)單一登錄,防止同一賬號(hào)重復(fù)登錄,有點(diǎn)類似于qq登錄踢人效果,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
Java中的HashMap弱引用之WeakHashMap詳解
這篇文章主要介紹了Java中的HashMap弱引用之WeakHashMap詳解,當(dāng)內(nèi)存空間不足,Java虛擬機(jī)寧愿拋出OutOfMemoryError錯(cuò)誤,使程序異常終止,也不會(huì)靠隨意回收具有強(qiáng)引用的對(duì)象來解決內(nèi)存不足的問題,需要的朋友可以參考下2023-09-09
java中統(tǒng)一返回前端格式及統(tǒng)一結(jié)果處理返回詳解
這篇文章主要介紹了統(tǒng)一結(jié)果處理的重要性,以及如何在SpringBoot項(xiàng)目中定義和使用統(tǒng)一結(jié)果返回類,通過構(gòu)造器私有化和靜態(tài)方法ok、error,提供了成功和失敗的統(tǒng)一響應(yīng)格式,需要的朋友可以參考下2025-02-02
IDEA搭建純注解版本SpringMVC的web開發(fā)環(huán)境全過程并分析啟動(dòng)原理
本文詳細(xì)介紹了如何使用注解開發(fā)搭建Spring Web環(huán)境,包括創(chuàng)建Maven工程、配置web環(huán)境、設(shè)置pom.xml、創(chuàng)建配置類和控制器等步驟,同時(shí),文章還探討了注解開發(fā)中如何創(chuàng)建IOC容器和添加DispatcherServlet組件,并通過Servlet 3.0規(guī)范2024-11-11
Mybatis實(shí)現(xiàn)一對(duì)多映射處理
MyBatis是一種流行的Java持久化框架,這篇文章主要為大家介紹了Mybatis如何實(shí)現(xiàn)一對(duì)多映射處理,文中的示例代碼講解詳細(xì),需要的可以參考下2023-08-08
Springboot 跨域配置無效及接口訪問報(bào)錯(cuò)的解決方法
這篇文章主要介紹了Springboot 跨域配置無效及接口訪問報(bào)錯(cuò)的解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
java實(shí)例方法被覆蓋,靜態(tài)方法被隱藏Explain(詳解)
下面小編就為大家?guī)硪黄猨ava實(shí)例方法被覆蓋,靜態(tài)方法被隱藏Explain(詳解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05

