Java ASM詳解:類的結(jié)構(gòu)(一)
了解了各個(gè)字節(jié)碼的意義,我們可以構(gòu)建出方法。這篇文章開始不再講具體的字節(jié)碼,而是開始介紹類的結(jié)構(gòu)。今天這篇文章主要講類的成員/屬性和它們?cè)谧止?jié)碼中的寫入表示。
一.類靜態(tài)初始化方法
當(dāng)一個(gè)類被裝載入內(nèi)存,它是沒有靜態(tài)初始化過的。當(dāng)從其他類調(diào)用它內(nèi)部的方法或字段時(shí),類才會(huì)被靜態(tài)初始化。靜態(tài)初始化只進(jìn)行一次。
靜態(tài)初始化的主要工作是在類加載之后使用之前進(jìn)行類靜態(tài)數(shù)據(jù)的初始化操作。在Java代碼中,它使用static塊聲明,一個(gè)類文件可以有多個(gè)static塊。
在字節(jié)碼中,靜態(tài)初始化會(huì)被寫入成為一個(gè)方法,名稱為<clinit>,是Class Initialization的縮寫。它的描述符要求是()V,不帶有泛型簽名,不拋出異常,訪問標(biāo)志必須含有static。如果<clinit>不滿足這些條件,會(huì)產(chǎn)生下面這些報(bào)錯(cuò):

和static塊不同,在字節(jié)碼中<clinit>只能存在一個(gè)。多個(gè)static塊合成一個(gè)<clinit>會(huì)按照static塊的順序一塊一塊進(jìn)行拼接,同時(shí)局部變量也會(huì)進(jìn)行拼接。例如下方的代碼:
在字節(jié)碼中會(huì)進(jìn)行拼接,翻譯后變成這樣:
二.構(gòu)造函數(shù)
創(chuàng)建某個(gè)類的對(duì)象必然會(huì)調(diào)用某一個(gè)具體的構(gòu)造函數(shù)。構(gòu)造函數(shù)的意義就是對(duì)類對(duì)象內(nèi)部數(shù)據(jù)進(jìn)行初始化。
在字節(jié)碼中,構(gòu)造函數(shù)的名稱是<init>而非類名。它要求返回值是void(V),訪問標(biāo)志只包含訪問權(quán)限標(biāo)志(public/protected/private),對(duì)于參數(shù)列表和異常列表不加限制。如果不滿足上面的條件,JVM在加載階段會(huì)拋出下面的異常:

構(gòu)造函數(shù)的另一項(xiàng)限制在它的內(nèi)部。構(gòu)造函數(shù)必須調(diào)用它父類的構(gòu)造函數(shù)或本類的另一個(gè)構(gòu)造函數(shù),否則會(huì)在驗(yàn)證時(shí)拋出java.lang.VerifyError: Constructor must call super() or this() before return
。
每個(gè)類都要含有一個(gè)構(gòu)造函數(shù)。如果源碼中沒有構(gòu)造函數(shù),那么編譯時(shí)會(huì)自動(dòng)添加默認(rèn)構(gòu)造函數(shù),它的Java源碼和字節(jié)碼寫入如下:
【注意:這里的代碼不適用于非靜態(tài)內(nèi)部類,下文會(huì)詳細(xì)介紹】
三.類靜態(tài)字段的初始化
類中的靜態(tài)字段不和類對(duì)象綁定而和類綁定,因此它們必須在靜態(tài)初始化時(shí)被初始化;而類中的實(shí)例字段和對(duì)象綁定,需要在對(duì)象構(gòu)造時(shí)被初始化。
如果初始化語句是一個(gè)常量字面值且字段是靜態(tài)字段,如字符串、數(shù)字、null或XXXX.class,它們的初始化字節(jié)碼應(yīng)該直接使用visitField的value參數(shù)將初始值傳入。
如果初始化語句不是常量字面值或不是靜態(tài)字段,如調(diào)用方法語句、對(duì)象創(chuàng)建、數(shù)組創(chuàng)建等,它們的初始化字節(jié)碼應(yīng)該在<clinit>或<init>方法內(nèi)。
根據(jù)這個(gè)規(guī)則,我們可以推斷出一段靜態(tài)字段初始化的代碼的具體實(shí)現(xiàn):
在寫入字節(jié)碼時(shí),Java代碼應(yīng)該是這樣的:
寫入字節(jié)碼的代碼:
在字節(jié)碼中,靜態(tài)初始化方法內(nèi)可以對(duì)一個(gè)靜態(tài)常量字段進(jìn)行多次賦值,并且JVM不報(bào)錯(cuò)。如果在靜態(tài)初始化中不存在初始化某個(gè)靜態(tài)字段的代碼,那么它們就會(huì)使用默認(rèn)值,也就是visitField中value參數(shù)決定的值。
實(shí)例字段的初始化類似于靜態(tài)初始化,只是它們?cè)跇?gòu)造函數(shù)內(nèi)寫入。
四. 橋接方法
在介紹橋接方法(Bridge Method)之前,先來簡(jiǎn)單介紹重寫(Override)。
重寫就是子類將父類的某個(gè)方法進(jìn)行覆蓋,進(jìn)而實(shí)際執(zhí)行時(shí)會(huì)執(zhí)行子類方法而不是父類的。重寫需要滿足:
名稱相同:父類的方法名稱必須與子類的方法名稱相同。
參數(shù)列表對(duì)應(yīng):父類的方法參數(shù)列表應(yīng)該與子類一一對(duì)應(yīng),這一點(diǎn)適用于泛型,也就是子類確定的類型參數(shù)應(yīng)該在重寫方法中帶入類型參數(shù)確定的類型。
異常列表不增添:子類的復(fù)寫方法不能出現(xiàn)父類沒有聲明拋出的異常。
訪問權(quán)限不縮小:子類的復(fù)寫方法的可見性不能低于父類方法可見性,如父類的訪問可見性為public,那么子類也必須聲明為public。
下面是具體的例子,下方的重寫案例都是正常能通過編譯的:
之前我們說到,泛型的實(shí)現(xiàn)是所謂的泛型擦除,也就是類型參數(shù)會(huì)被擦除到其限定的父類上。現(xiàn)在來看看test2這個(gè)方法,在父類和子類中,它們的方法描述符和泛型簽名是不一樣的:
雖然描述符不同,但是在邏輯上已經(jīng)達(dá)成了重寫條件,應(yīng)該當(dāng)作重寫處理。但是,因?yàn)榉椒枋龇煌瑢?shí)際調(diào)用時(shí)JVM是找不到這個(gè)方法的:invokevirtual字節(jié)碼只會(huì)尋找名稱相同且方法描述符相同的方法。因此,橋接方法出現(xiàn)用于解決這個(gè)問題。它的代碼意義就是將確定的類型參數(shù)強(qiáng)制轉(zhuǎn)換,將父類泛型化的參數(shù)傳入具體化的子類復(fù)寫方法中。
例如test2,編譯器給出的橋接方法就像下面這樣:
橋接方法僅出現(xiàn)在父類方法和子類重寫方法擦除后的方法描述符不一致時(shí),如下方的例子:
橋接方法擁有下面的特性:
名稱與方法描述符相同:為了invokevirtual字節(jié)碼能成功定位到這個(gè)重寫方法,橋接方法必須和父類的目標(biāo)方法名稱和描述符一致。
訪問標(biāo)志帶有ACC_SYNTHETIC和ACC_BRIDGE標(biāo)志。
訪問權(quán)限和子類重寫方法相同。
異常列表和父類方法相同。
接下來我們要使用字節(jié)碼實(shí)現(xiàn)BaseClass:
字節(jié)碼:
五.內(nèi)部類
類的內(nèi)部成員除了字段和方法外,還有內(nèi)部類。內(nèi)部類分為兩種:
靜態(tài)內(nèi)部類:使用static修飾的內(nèi)部類。內(nèi)部接口、內(nèi)部枚舉默認(rèn)帶有static訪問標(biāo)志,因此它們也屬于靜態(tài)內(nèi)部類。
非靜態(tài)內(nèi)部類:不使用static修飾的內(nèi)部類。
內(nèi)部類的字節(jié)碼數(shù)據(jù)不應(yīng)該在外部類中的類文件數(shù)據(jù)中出現(xiàn),應(yīng)該獨(dú)立于外部類。命名方式為"外部類名稱$內(nèi)部類名稱";如果內(nèi)部類是局部?jī)?nèi)部類,在內(nèi)部類名稱前還需要加上編號(hào);如果內(nèi)部類是一個(gè)匿名內(nèi)部類,應(yīng)該使用編號(hào)代替。
雖然內(nèi)部類不需要寫在外部類文件里面,但是外部類文件還是要聲明它的。聲明使用ClassWriter的visitInnerClass方法,它的每個(gè)參數(shù)的意義如下:
name - 內(nèi)部類的全限定名。例如test.Test下的內(nèi)部類Inner這項(xiàng)值就是"test/Test$Inner"。
outerName - 外部類的全限定名,如果內(nèi)部類是匿名內(nèi)部類或局部?jī)?nèi)部類,這項(xiàng)是null。
innerName - 內(nèi)部類的名稱,如果內(nèi)部類是匿名內(nèi)部類,這項(xiàng)是null。
access - 內(nèi)部類的訪問標(biāo)志。
最外層外部類需要寫出它內(nèi)部所有的類,包括嵌套的內(nèi)部類。
下面是一些內(nèi)部類和它們的聲明:
在Java 11,JEP 181(Nest-Based Access Control)加入了NestHost和NestMember兩項(xiàng)屬性用于輔助訪問權(quán)限控制,規(guī)定了所有內(nèi)部類(包括嵌套的內(nèi)部類)是最外層外部類的NestMember,最外層的外部類是所有內(nèi)部類(包括嵌套)的NestHost。
聲明NestMember使用ClassWriter的visitNestMember方法,參數(shù)是內(nèi)部類的全限定名。寫入它的字節(jié)碼如下(仍然使用上方的代碼):
說回到內(nèi)部類文件,它也需要聲明外部類和NestHost。聲明外部類也使用visitInnerClass方法,需要寫出所有的外部類,包括嵌套;聲明NestHost使用visitNestHost方法,參數(shù)是最外層外部類全限定名。
下面是Inner1聲明外部類的字節(jié)碼寫入:
內(nèi)部類的聲明到此為止,接下來看看內(nèi)部類和外部類的不同之處。
對(duì)于非靜態(tài)內(nèi)部類,它的類對(duì)象需要依托于一個(gè)外部類實(shí)例才能創(chuàng)建。例如下方的代碼:
非靜態(tài)內(nèi)部類保存了外部類的實(shí)例,保存的字段名稱是this$嵌套類深度-1(如果名稱已存在那么就在這個(gè)名字后加$直到不存在有這個(gè)名稱的字段),以InnerClass舉例,它的字節(jié)碼實(shí)際上類似于這樣:
外部類實(shí)例字段要求訪問標(biāo)志是ACC_FINAL和ACC_SYNTHETIC。使用類名.this相當(dāng)于使用這個(gè)字段逐級(jí)獲取,下面兩個(gè)代碼等價(jià):
為了適應(yīng)外部類實(shí)例字段的加入,非靜態(tài)內(nèi)部類的構(gòu)造函數(shù)和普通的構(gòu)造函數(shù)不同。它的第一個(gè)局部變量仍然是this,但是第二個(gè)局部變量(或者說是第一個(gè)形式參數(shù))成為了外部類的實(shí)例,從第二個(gè)形式參數(shù)開始才是真正在源碼層級(jí)的參數(shù)列表。內(nèi)部類的默認(rèn)構(gòu)造函數(shù)如下(使用InnerClass舉例)。
字節(jié)碼如下:
六.密封類
密封類(Sealed Class)于Java 15(JEP 360)被加入,它限制了類可被哪些類繼承。下面是例子:
聲明密封類屬性只需要ClassWriter的visitPermittedSubclass,參數(shù)是子類全限定名。上面的SupClass字節(jié)碼可以這樣寫入:

類的基本結(jié)構(gòu)就到這里,下一期:類的結(jié)構(gòu)(二),主要講解枚舉、記錄等特殊的類結(jié)構(gòu)。
有問題在評(píng)論區(qū)指出。