嚴(yán)忠林
?
Java動(dòng)態(tài)綁定的方法重載的實(shí)現(xiàn)
嚴(yán)忠林
摘要:Java支持“方法重載”,但其執(zhí)行代碼是在編譯時(shí)就確定的,不能根據(jù)運(yùn)行時(shí)的實(shí)際對(duì)象動(dòng)態(tài)改變,這有時(shí)會(huì)增加代碼的復(fù)雜性。通過(guò)使用JSR-292提供的功能,可以實(shí)現(xiàn)一個(gè)框架,讓Java擁有在運(yùn)行時(shí)綁定重載代碼的能力??梢蕴岣叱绦虻暮?jiǎn)明性、可重用性和可擴(kuò)展性。
關(guān)鍵詞:方法重載;方法重寫(xiě);動(dòng)態(tài)調(diào)用指令;方法句柄;Java類(lèi)文件處理
作為面向?qū)ο笳Z(yǔ)言,Java支持“方法重寫(xiě)(Method Overriding)”。針對(duì)同一方法簽名,父類(lèi)和各個(gè)子類(lèi)可以提供不同的實(shí)現(xiàn)代碼,運(yùn)行時(shí)將根據(jù)實(shí)際對(duì)象的不同自動(dòng)選擇。它是實(shí)現(xiàn)多態(tài)性的基礎(chǔ),使同樣的代碼,在不同的對(duì)象上運(yùn)行,即可獲得不同的結(jié)果。通過(guò)添加新的子類(lèi),不改變已有代碼,就能擴(kuò)展系統(tǒng)功能,大大提高了軟件開(kāi)發(fā)的效率和質(zhì)量。這是面向?qū)ο蠓椒▽?shí)現(xiàn)代碼可重用、可擴(kuò)展的基本手段。
在語(yǔ)法上,Java也支持“方法重載(Method Overloading)”。在一個(gè)類(lèi)中,可以為同一方法名但不同的參數(shù)編寫(xiě)不同的方法體,程序能依據(jù)調(diào)用時(shí)參數(shù)的類(lèi)型、數(shù)量、次序,選擇相應(yīng)的代碼執(zhí)行。從概念上可以認(rèn)為這些方法體進(jìn)行的是同一處理,但卻可根據(jù)外界提供的不同條件“隨機(jī)應(yīng)變”。這對(duì)提高程序的易讀易寫(xiě)性很有幫助。
方法重號(hào)方法重載兩種方式都提高了編程的方便、靈活性,但它們的執(zhí)行機(jī)制是完全不同的[1]。對(duì)于所執(zhí)行方法體的確定時(shí)機(jī),重寫(xiě)采用的是“動(dòng)態(tài)綁定”或叫“晚捆綁”。它是直到運(yùn)行時(shí),才依據(jù)方法棧中首個(gè)參數(shù)引用的對(duì)象,來(lái)選擇應(yīng)該執(zhí)行的代碼(在Java源代碼中無(wú)此參數(shù),但編譯后的bytecode會(huì)將消息的接受者作為首個(gè)參數(shù)送入方法棧)。而重載則是“靜態(tài)綁定”,所執(zhí)行方法體是在編譯時(shí)根據(jù)參數(shù)類(lèi)型、數(shù)量等不同確定的,運(yùn)行時(shí)沒(méi)有選擇,直接運(yùn)行。對(duì)于方法體的選擇依據(jù),兩者也不同。重寫(xiě)采用的是“單分派”,只根據(jù)棧幀中首個(gè)參數(shù)的不同來(lái)做選擇。而重載是“多分派”,能綜合所有參數(shù)的情況加以選擇。
兩種方式各有特點(diǎn)。重載高效,雖然要參照多個(gè)參數(shù),但所有工作都在編譯時(shí)完成,運(yùn)行時(shí)無(wú)額外開(kāi)銷(xiāo)。但由于無(wú)法利用運(yùn)行時(shí)信息,操作完全固定,所以靈活性不夠。而重寫(xiě)正好相反,運(yùn)行時(shí)需進(jìn)行選擇,有一定性能損失。但相同的代碼,卻能依據(jù)運(yùn)行時(shí)情況,執(zhí)行不同的處理。
目前Java等語(yǔ)言對(duì)重寫(xiě)方式都選擇單分派進(jìn)行。既能提供靈活性,運(yùn)行時(shí)又只需最簡(jiǎn)單的處理(一般是通過(guò)查找存儲(chǔ)于類(lèi)中的虛方法表來(lái)獲得方法入口地址),可最大限度地保證運(yùn)行效率。綜合各方面考慮,這確實(shí)是最佳選擇。但在某些特定情況下,單分派提供的靈活性仍然不夠,有時(shí)也會(huì)增加程序的復(fù)雜性,不利于程序的擴(kuò)展。
舉個(gè)簡(jiǎn)單例子,假如要處理幾何問(wèn)題,定義了采用直角坐標(biāo)表示的點(diǎn)(Class Point)和線(xiàn)(Class Line)。Line類(lèi)中用k、b字段記錄直線(xiàn)的斜率和截距,許多運(yùn)算都要使用它們。但對(duì)平行于y軸的直線(xiàn),k、b在運(yùn)算時(shí)無(wú)意義,需要再定義一個(gè)子類(lèi)VLine,添加字段a記錄x軸的截距。它的許多處理和普通直線(xiàn)運(yùn)算不同,因此需要重寫(xiě)這些方法。
現(xiàn)在要求兩條直線(xiàn)的交點(diǎn),針對(duì)不同類(lèi)型的直線(xiàn)有不同的計(jì)算方法,在Line和VLine兩個(gè)類(lèi)中都要分別重載實(shí)現(xiàn)它們,假如是crossPoint(Line l)和crossPoint(VLine l)?,F(xiàn)在main()中有兩個(gè)直線(xiàn)數(shù)組,用雙重循環(huán)重復(fù)執(zhí)行語(yǔ)句①,應(yīng)該能求出它們所有的交點(diǎn),如圖1所示:
但由于重載的靜態(tài)限制,編譯時(shí)就根據(jù)聲明綁定了Line型參數(shù)的方法,而運(yùn)行時(shí)l2卻可以是VLine對(duì)象,所以語(yǔ)句①并不正確。
這類(lèi)問(wèn)題的傳統(tǒng)解決方案是在代碼中添加if語(yǔ)句,對(duì)參數(shù)類(lèi)型加以判斷,再選擇執(zhí)行不同的處理。然而這已經(jīng)失去了面向?qū)ο蟮囊馊?。?duì)于這兒的簡(jiǎn)單情況,此方案當(dāng)然無(wú)傷大雅。但假如系統(tǒng)的繼承關(guān)系、要進(jìn)行的處理都比較復(fù)雜,那么繁復(fù)的if結(jié)構(gòu)將使代碼變得龐大臃腫,難讀難改,成為程序員的負(fù)擔(dān)。
更重要的是這種處理方式不具有可擴(kuò)展性。像上述的幾何系統(tǒng),假如在完成以后,又需要引入極坐標(biāo)表示法和相應(yīng)處理。這時(shí)僅僅添加新的點(diǎn)、線(xiàn)子類(lèi)是不夠的,仍然不得不打開(kāi)所有已完成的源代碼,在眾多類(lèi)中,毫無(wú)遺漏地找出相關(guān)if語(yǔ)句,正確地進(jìn)行修改添加。對(duì)于一個(gè)復(fù)雜系統(tǒng),這決不是一件輕松的工作。
圖1 示例代碼
實(shí)際上,這類(lèi)問(wèn)題的最佳解決方案是“多分派+動(dòng)態(tài)綁定”,即使用直到運(yùn)行時(shí)才進(jìn)行綁定的方法重載機(jī)制。如果有這樣的機(jī)制,系統(tǒng)就會(huì)根據(jù)運(yùn)行時(shí)各參數(shù)的實(shí)際情況,自動(dòng)選擇最合適的方法執(zhí)行,代碼中不再需要類(lèi)型判定,添加新的子類(lèi)也因此變得簡(jiǎn)單。然而,方法何時(shí)綁定、如何綁定,是編程語(yǔ)言的內(nèi)部機(jī)制,是由語(yǔ)言的設(shè)計(jì)者決定的。在過(guò)去,語(yǔ)言使用者是完全無(wú)能為力的。不過(guò),自JDK1.7起,實(shí)現(xiàn)了動(dòng)態(tài)類(lèi)型語(yǔ)言支持(JSR-292),這方面情況已有所改觀。
JSR-292[2]的最初出發(fā)點(diǎn)是為了便于在JVM虛擬機(jī)上,高效實(shí)現(xiàn)類(lèi)似于JRuby、Jython、Groovy、Clojure這樣的動(dòng)態(tài)類(lèi)型語(yǔ)言。動(dòng)態(tài)類(lèi)型語(yǔ)言不需要變量聲明,變量類(lèi)型在運(yùn)行時(shí)確定,并據(jù)此進(jìn)行不同的處理。例如對(duì)于表達(dá)式a+b,要等到運(yùn)行到此處時(shí),才會(huì)根據(jù)a、b類(lèi)型確定是做整數(shù)加、浮點(diǎn)數(shù)加、還是字符串加。這類(lèi)語(yǔ)言簡(jiǎn)潔、靈活,近來(lái)越來(lái)越受歡迎,但JVM對(duì)它們的支持一直有所欠缺。
JVM的bytecode原有4條方法調(diào)用指令[3],invokevirtual、invokespecial、invokeinterface和invokestatic,分別是調(diào)用動(dòng)態(tài)綁定的成員方法、不需動(dòng)態(tài)綁定的成員方法、接口定義的方法和類(lèi)方法。它們最終執(zhí)行的方法體都是由虛擬機(jī)根據(jù)編譯時(shí)嵌入指令中的符號(hào)引用,按照固定的查找規(guī)則確定的。它們適用于Java語(yǔ)言,但其他語(yǔ)言要按照自己的規(guī)則動(dòng)態(tài)確定執(zhí)行的方法體,就不那么容易了,需要使用種種特異手法去遷就JVM中的這些規(guī)則。這勢(shì)必增加語(yǔ)言實(shí)現(xiàn)的復(fù)雜度,也帶來(lái)額外的性能和內(nèi)存開(kāi)銷(xiāo)。因此JSR-292引入了一條新的調(diào)用指令invokedynamic,允許其他語(yǔ)言實(shí)現(xiàn)者按照自己的調(diào)用規(guī)則,確定最終執(zhí)行的代碼,從而直接在虛擬機(jī)層面上解決了此問(wèn)題。
為新增加的invokedynamic指令,JVM在類(lèi)文件中還添加了一些描述它的新的常量、屬性元素。又在API中引入了java.lang.invoke包[4],提供支持這些操作的相關(guān)類(lèi)。其中最重要的是方法句柄類(lèi)(MethodHandle),它提供在虛擬機(jī)底層訪問(wèn)類(lèi)或?qū)ο笾懈鞒蓡T的機(jī)制,無(wú)論是方法調(diào)用還是數(shù)據(jù)訪問(wèn),都直接加以處理,甚至連其他調(diào)用指令都要執(zhí)行的權(quán)限校驗(yàn)等行為也會(huì)跳過(guò)。對(duì)于現(xiàn)今日益發(fā)展的JIT、Hotspot等技術(shù)帶來(lái)的豐富的優(yōu)化措施,它們也能夠享用。因而用它們進(jìn)行處理,能保持代碼執(zhí)行的高效率。MethodHandle對(duì)象是強(qiáng)類(lèi)型的,由MethodType描述它的參數(shù)和返回值類(lèi)型。使用時(shí),提供的參數(shù)應(yīng)滿(mǎn)足其類(lèi)型要求,當(dāng)然它也有能進(jìn)行各種類(lèi)型轉(zhuǎn)換的方法,用戶(hù)可根據(jù)需要選用。java.lang.invoke包中還提供了許多生成、變換、組合MethodHandle的方法,使用者可根據(jù)自己實(shí)現(xiàn)的規(guī)則的要求,在使用前后作各種處理,達(dá)成希望的結(jié)果。
該包中還有CallSite類(lèi),每個(gè)invokedynamic指令中都含有此對(duì)象的引用,它記錄著虛擬機(jī)運(yùn)行至此處時(shí)要執(zhí)行的方法句柄。初次運(yùn)行時(shí),該引用是空的,虛擬機(jī)要執(zhí)行程序中先前嵌入的自舉方法句柄BootStrapMethodHandle,根據(jù)運(yùn)行時(shí)信息,生成相應(yīng)對(duì)象,確定以后將執(zhí)行的動(dòng)作。
利用這些類(lèi)和對(duì)象,可以為虛擬機(jī)制定動(dòng)態(tài)調(diào)用的各種規(guī)則。利用它們,也可實(shí)現(xiàn)動(dòng)態(tài)綁定的方法重載。具體思路是在執(zhí)行這些方法調(diào)用時(shí),首先,查看運(yùn)行棧中各參數(shù)的實(shí)際類(lèi)型,依據(jù)這些信息,查找到最匹配的方法體,再轉(zhuǎn)入執(zhí)行。為了和Java原來(lái)的調(diào)用機(jī)制一致,匹配時(shí)以排在前面的參數(shù)優(yōu)先為原則,即先找第一個(gè)參數(shù)的最匹配類(lèi),再找第二個(gè),以此類(lèi)推。為盡量減少對(duì)代碼執(zhí)行效率造成的影響,這種搜尋工作不宜在運(yùn)行時(shí)進(jìn)行。因此,要事先將參數(shù)類(lèi)型的各種可能組合與方法句柄的對(duì)應(yīng)放入一個(gè)HashMap,到運(yùn)行時(shí)只要用實(shí)際類(lèi)型做一次散列映射,就能獲得最匹配的方法。這樣就實(shí)現(xiàn)了基于多分派的運(yùn)行時(shí)進(jìn)行的方法捆綁,即動(dòng)態(tài)的方法重載。如在此機(jī)制下執(zhí)行圖1中的求兩線(xiàn)交點(diǎn)的語(yǔ)句①,不管l1、l2為何類(lèi)型,都能得到正確結(jié)果。
這種基于棧中參數(shù)類(lèi)型進(jìn)行的映射,還可以處理得更靈活,使通過(guò)添加子類(lèi)來(lái)擴(kuò)展系統(tǒng)的工作更簡(jiǎn)單方便。仍以求兩線(xiàn)交點(diǎn)為例,假如在直角坐標(biāo)系統(tǒng)完成以后,要添加對(duì)極坐標(biāo)的支持,當(dāng)然要添加新的點(diǎn)、線(xiàn)子類(lèi)(class P_Point和class P_Line)。為使語(yǔ)句①依然在任何情況下都能正確運(yùn)行,除了在新的P_Line類(lèi)中需有重載各個(gè)crossPoint方法以外,在舊的Line和VLine類(lèi)中仍要添加新的Point crossPoint(P_Line l)方法,是否能不修改已有代碼呢?
現(xiàn)在完全可以通過(guò)在新類(lèi)中定義靜態(tài)方法來(lái)替代,如圖2所示:
圖2 新添加的子類(lèi)
定義static Point crossPoint(Line la, P_Line lb)方法,將所有對(duì)((Line)l1).crosspoint((P_Line)l2)的調(diào)用映射為對(duì)此方法的調(diào)用。這兩種方法調(diào)用,方法棧中的參數(shù)結(jié)構(gòu)完全一樣。本來(lái)因?yàn)槔墮C(jī)制不同,需用不同的調(diào)用指令,所以不能混用。但使用這里自定義的映射方案,卻可以同樣處理。這樣所有的擴(kuò)展工作就只出現(xiàn)在新建的子類(lèi)中,在新類(lèi)中為舊的類(lèi)添加它所缺少的方法,舊的代碼可以完全保持不變。這對(duì)于新系統(tǒng)的實(shí)現(xiàn)和維護(hù),無(wú)疑是有好處的。
JSR-292是專(zhuān)為支持新的動(dòng)態(tài)語(yǔ)言設(shè)計(jì)的,沒(méi)準(zhǔn)備用于Java,所以在高級(jí)語(yǔ)言層面,通過(guò)Java源代碼不能直接生成invokedynamic指令和相應(yīng)結(jié)構(gòu)。所有工作必須在bytecode層面,通過(guò)修改編譯生成的類(lèi)文件進(jìn)行。好在JVM的類(lèi)文件有嚴(yán)格定義的結(jié)構(gòu),所有的類(lèi)、變量、方法,包括參數(shù)信息等都通過(guò)字符串構(gòu)成的明晰符號(hào)來(lái)說(shuō)明和引用[3]。只要熟悉這些結(jié)構(gòu)和bytecode指令,通過(guò)添加、修改相應(yīng)元素,依然可以達(dá)成需要功能。
為了明確處理對(duì)象,制定了DynamicMethod標(biāo)注,采用類(lèi)文件的符號(hào)串格式,說(shuō)明將采用動(dòng)態(tài)重載機(jī)制方法的最上層類(lèi)名/接口名、方法名及參數(shù)如圖3所示:
圖3 聲明采用新機(jī)制的方法的標(biāo)注及示例
它用于啟動(dòng)類(lèi)前。處理程序可以從啟動(dòng)類(lèi)開(kāi)始,掃描項(xiàng)目中由編譯器生成的各個(gè)類(lèi)文件。記錄它們的繼承關(guān)系、出現(xiàn)的所有方法實(shí)現(xiàn)、這些方法的各個(gè)調(diào)用點(diǎn)等等。根據(jù)這些信息,處理程序要完成下列操作:
將對(duì)原方法的調(diào)用指令invokevirtual或invokeinterface改為invokedynamic,其前后的參數(shù)準(zhǔn)備和結(jié)果返回不需修改。但invokevirtual指令和invokedynamic指令長(zhǎng)度不一致,后續(xù)指令要作相應(yīng)偏移。由此又會(huì)引起方法中局部變量、StackMap等多個(gè)屬性的有效范圍和轉(zhuǎn)移偏移量、代碼長(zhǎng)度等數(shù)據(jù)的修改。
對(duì)每個(gè)invokedynamic指令,要指定其自舉方法句柄。為此要在類(lèi)文件的常量池中加入多種類(lèi)型的常量,如InvokeDynamic_info、MethodHandle_info等,在類(lèi)屬性池中加入BootstrapMethods屬性,以滿(mǎn)足invokedynamic指令執(zhí)行的需要。
還要建立一個(gè)輔助類(lèi),提供映射、自舉等需要的處理代碼。因?yàn)橐呀?jīng)在編譯之后,所以它要在bytecode層面直接生成。實(shí)際上各功能的具體執(zhí)行過(guò)程對(duì)不同的應(yīng)用是非常類(lèi)似的,只是在類(lèi)名、它們的繼承關(guān)系、方法名、參數(shù)個(gè)數(shù)等方面有所不同,所以可以通過(guò)對(duì)一個(gè)bytecode模版進(jìn)行相應(yīng)的替換、重復(fù),生成所需要的代碼。在這一類(lèi)中主要應(yīng)包含下列方法:
根據(jù)方法簽名及各個(gè)類(lèi)的繼承、實(shí)現(xiàn)信息,獲得各方法句柄,并與適當(dāng)?shù)念?lèi)型組合一起,建立HashMap的方法。
運(yùn)行時(shí)獲得實(shí)際參數(shù)類(lèi)型,完成動(dòng)態(tài)綁定并調(diào)用執(zhí)行的方法。
指令首次調(diào)用時(shí)執(zhí)行的自舉方法。
對(duì)前述求直線(xiàn)交點(diǎn)的例子作如此處理,將大致生成的代碼如圖4所示:
圖4 自動(dòng)生成的輔助類(lèi)
這一對(duì)類(lèi)文件的修改處理步驟可以在編譯后單獨(dú)執(zhí)行,保存獲得的結(jié)果后再加以運(yùn)行。也可以和程序的執(zhí)行合在一起,在主程序main()啟動(dòng)前,代碼裝載時(shí)加以變換,獲得的類(lèi)代碼直接運(yùn)行。這一方案特別適宜于試驗(yàn)、調(diào)試階段,雖然啟動(dòng)稍慢,但可重復(fù)嘗試、修改。它要利用Java.lang.instrument包[4]進(jìn)行,將對(duì)類(lèi)代碼的轉(zhuǎn)換過(guò)程實(shí)現(xiàn)為ClassFileTransformer接口的transform方法,該方法會(huì)在類(lèi)裝載器裝載了新類(lèi),對(duì)其進(jìn)行合法性驗(yàn)證之前執(zhí)行。在main()執(zhí)行前,它可先行載入需要的類(lèi),收集信息,完成各種變換。用戶(hù)還要提供一個(gè)包含premain方法的類(lèi),在其中用虛擬機(jī)提供的instrumentation對(duì)象注冊(cè)該transform方法。系統(tǒng)啟動(dòng)時(shí)用適當(dāng)?shù)拿钚羞x項(xiàng),使其在應(yīng)用程序的main方法之前執(zhí)行。這樣就可以使程序的所有類(lèi)文件在完成了希望的變換后執(zhí)行。
通過(guò)使用JSR-292提供的功能,實(shí)現(xiàn)了在運(yùn)行時(shí)才確定執(zhí)行代碼的方法重載機(jī)制。這對(duì)于有大量子類(lèi),而處理方式不能取決于單個(gè)對(duì)象,需要綜合考慮參與處理的所有實(shí)際對(duì)象,才能最終確定的復(fù)雜系統(tǒng),是很有意義的。執(zhí)行代碼的選擇,由計(jì)算機(jī)自動(dòng)執(zhí)行,程序員不再需要重復(fù)編寫(xiě)那些繁瑣的控制結(jié)構(gòu),使代碼簡(jiǎn)潔清晰,易讀易改,提高工作效率。這樣的系統(tǒng)也更可重用、更易擴(kuò)展,更加符合面向?qū)ο蠓椒ㄋ非蟮能浖_(kāi)發(fā)目標(biāo)。
這種機(jī)制的使用,只需要在代碼中增加一個(gè)標(biāo)記。雖然實(shí)際上改變了Java的運(yùn)行機(jī)制,但在語(yǔ)法上沒(méi)有任何改變,執(zhí)行結(jié)果也符合預(yù)期,不違背原有的習(xí)慣,因此不會(huì)給程序員造成任何負(fù)擔(dān)。它對(duì)運(yùn)行效率的影響,也只是增加一次散列表的查找,在現(xiàn)代運(yùn)行環(huán)境下應(yīng)該是可以接受的。
參考文獻(xiàn)
[1] James Gosling, Bill Joy. The Java Language Specification Java SE 8 Edition [M]. Addison-Wesley Professional, 2015,2.
[2] John Rose. JSR-292 Supporting Dynamically Typed Languages on the Java Platform (FinalRelease) [EB/OL]. https://jcp.org/aboutJava/communityprocess/final/jsr292/.
[3] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification Java SE 8 Edition [M]. Addison-Wesley Professional ,2015,2.
[4] Oracle co. Java Platform, Standard Edition 8 API Specification [EB/OL]. http:// docs.oracle.com/javase/ 8/docs/api/.
收稿日期:(2015.04.09)
作者簡(jiǎn)介:嚴(yán)忠林(1964-),男,江蘇鎮(zhèn)江,上海師范大學(xué),講師,碩士,研究方向:Java應(yīng)用、計(jì)算機(jī)體系結(jié)構(gòu),上海,200234
文章編號(hào):1007-757X(2015)12-0069-03
中圖分類(lèi)號(hào):TP311
文獻(xiàn)標(biāo)志碼:A