Java和JVM的重載識別,重寫方法是怎樣進行的
可變長參數(shù)方法的重載造成的。
1.案例
void invoke(Object obj, Object... args) { ... } void invoke(String s, Object obj, Object... args) { ... } invoke(null, 1); ? ?// 調(diào)用第二個invoke方法 invoke(null, 1, 2); // 調(diào)用第二個invoke方法 invoke(null, new Object[]{1}); // 只有手動繞開可變長參數(shù)的語法糖, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 才能調(diào)用第一個invoke方法
某API定義了兩個同名重載方法:
- 第一個接收一個Object,以及聲明為Object…的變長參數(shù)
- 第二個則接收一個String、一個Object,以及聲明為Object…的變長參數(shù)
想調(diào)用第一個方法,傳參(null, 1),即聲明為Object的形式參數(shù)所對應(yīng)的實際參數(shù)為null,而變長參數(shù)則對應(yīng)1。
之所以不提倡可變長參數(shù)方法重載,是因為Java編譯器可能無法決定應(yīng)該調(diào)用哪個目標方法。
這種情況下,編譯器會報錯,并且提示這方法調(diào)用有二義性。然而,Java編譯器直接將我的方法調(diào)用識別為調(diào)用第二個方法,這究竟是為什么呢?
Java虛擬機是怎么識別目標方法的?
2.重載與重寫
同一類中出現(xiàn)多個:
- 名字相同
- 參數(shù)類型相同
這限制可通過字節(jié)碼工具繞開,編譯完成后,可再向class文件中添加方法名和參數(shù)類型相同,而返回類型不同的方法。當這種包括多個方法名相同、參數(shù)類型相同,而返回類型不同的方法的類,出現(xiàn)在Java編譯器的用戶類路徑上時,它是怎么確定需要調(diào)用哪個方法的呢?
當前版本的Java編譯器會直接選取第一個方法名以及參數(shù)類型匹配的方法。并且,它會根據(jù)所選取方法的返回類型來決定可不可以通過編譯,以及需不需要進行值轉(zhuǎn)換等。
重載的方法在編譯過程中即可完成識別。具體到每一個方法調(diào)用,Java編譯器會根據(jù)所傳入?yún)?shù)的聲明類型(注意與實際類型區(qū)分)來選取重載方法。選取的過程共分為三個階段:
- 在不考慮對基本類型自動裝拆箱及可變長參數(shù)情況下選取重載方法
- 如在第1個階段沒找到適配方法,那在允許自動裝拆箱,但不允許可變長參數(shù)情況下選取重載方法
- 如在第2個階段中沒找到適配方法,那在允許自動裝拆箱及可變長參數(shù)情況下選取重載方法
如Java編譯器在同一階段中找到多個適配方法,那它會在其中選擇一個最為貼切,貼切程度關(guān)鍵就是形式參數(shù)類型的繼承關(guān)系。
傳入null時,它既可匹配第一個方法中聲明為Object的形式參數(shù),也可匹配第二個方法中聲明為String的形式參數(shù)。由于String是Object的子類,因此Java編譯器會認為第二個方法更貼切。
除同一個類中的方法,重載也可作用于這個類所繼承而來的方法。如子類定義了與父類中非私有方法同名的方法,且這兩個方法的參數(shù)類型不同,那在子類中,這兩個方法同樣構(gòu)成重載。
若子類定義與父類中非private方法的同名方法,且這兩方法參數(shù)類型相同,那這倆方法間啥關(guān)系:
- 若這倆都是static方法,那子類中的方法隱藏了父類中的方法
- 若都不是 static 的,則子類的方法重寫了父類中的方法
Java的方法重寫是多態(tài)的體現(xiàn):允許子類在繼承父類部分功能同時,擁有自己獨特行為。
重寫調(diào)用會根據(jù)調(diào)用者的動態(tài)類型選取實際的目標方法。
3.JVM的靜態(tài)綁定和動態(tài)綁定
Java虛擬機識別方法的關(guān)鍵在于類名、方法名及方法描述符(method descriptor)。
方法描述符由方法的參數(shù)類型及返回類型構(gòu)成。
同一類中,如同時出現(xiàn)多個名字相同且描述符相同的方法,那Java虛擬機會在類的驗證階段報錯。
Java虛擬機與Java語言不同,它不限制名字與參數(shù)類型相同,但返回類型不同的方法出現(xiàn)在同一類,對調(diào)用這些方法的字節(jié)碼,由于字節(jié)碼所附帶的方法描述符包含了返回類型,因此Java虛擬機能夠準確識別目標方法。
JVM方法重寫判定同樣基于方法描述符。
如子類定義了與父類中非私有、非靜態(tài)方法同名的方法,則僅當這倆方法的參數(shù)類型及返回類型一致,JVM才會判定為重寫。
對Java中重寫而Java虛擬機中非重寫的情況,編譯器會通過生成橋接方法[2]實現(xiàn)Java的重寫語義。
由于對重載方法的區(qū)分在編譯階段已完成,可認為JVM不存在重載概念。因此,某些文章將
- 重載稱為靜態(tài)綁定(static binding)或編譯時多態(tài)(compile-time polymorphism)
- 重寫稱為動態(tài)綁定(dynamic binding)
這說法在JVM語境下并非完全正確,因為某類中的重載方法可能被它的子類重寫,因此JVM 會將所有對非私有實例方法的調(diào)用編譯為需要動態(tài)綁定的類型。
JVM的:
- 靜態(tài)綁定指在解析時便能夠直接識別目標方法
- 動態(tài)綁定指要在運行過程中,根據(jù)調(diào)用者的動態(tài)類型來識別目標方法
Java字節(jié)碼中與調(diào)用相關(guān)的指令有:
- invokestatic:調(diào)用靜態(tài)方法
- invokespecial:調(diào)用私有實例方法、構(gòu)造器及使用super關(guān)鍵字調(diào)用父類的實例方法或構(gòu)造器,和所實現(xiàn)接口的默認方法
- invokevirtual:用于調(diào)用非私有實例方法
- invokeinterface:用于調(diào)用接口方法
- invokedynamic:用于調(diào)用動態(tài)方法較為復雜
編譯生成這四種調(diào)用指令的情況。
interface 客戶 { ? boolean isVIP(); } class 商戶 { ? public double 折后價格(double 原價, 客戶 某客戶) { ? ? return 原價 * 0.8d; ? } } class 奸商 extends 商戶 { ? @Override ? public double 折后價格(double 原價, 客戶 某客戶) { ? ? if (某客戶.isVIP()) { ? ? ? ? ? ? ? ? ? ? ? ? // invokeinterface ? ? ? ? ? ? return 原價 * 價格歧視(); ? ? ? ? ? ? ? ? ? ?// invokestatic ? ? } else { ? ? ? return super.折后價格(原價, 某客戶); ? ? ? ? ?// invokespecial ? ? } ? } ? public static double 價格歧視() { ? ? // 咱們的殺熟算法太粗暴了,應(yīng)該將客戶城市作為隨機數(shù)生成器的種子。 ? ? return new Random() ? ? ? ? ? ? ? ? ? ? ? ? ?// invokespecial ? ? ? ? ? ?.nextDouble() ? ? ? ? ? ? ? ? ? ? ? ? // invokevirtual ? ? ? ? ? ?+ 0.8d; ? } }
“商戶”類定義了一個成員方法,叫“折后價格”,它接收一個double類型參數(shù)及一個“客戶”類型參數(shù)。
這里“客戶”是個接口,定義了一個接口方法“isVIP”。
“奸商”類這個方法,首先調(diào)用客戶#isVIP,該調(diào)用會被編譯為invokeinterface指令
- 若客戶是VIP,則調(diào)用奸商類的一個名叫“價格歧視”的靜態(tài)方法。該調(diào)用會被編譯為invokestatic指令
- 如客戶不是VIP,則通過super調(diào)用父類的“折后價格”方法。該調(diào)用會被編譯為invokespecial指令
在靜態(tài)方法“價格歧視”會調(diào)用Random類的構(gòu)造器。該調(diào)用會被編譯為invokespecial指令。然后以這個新建Random對象為調(diào)用者,調(diào)用Random類中的nextDouble方法。該調(diào)用會被編譯為invokevirutal指令。
對于invokestatic以及invokespecial而言,Java虛擬機能夠直接識別具體的目標方法。
而對于invokevirtual以及invokeinterface而言,在絕大部分情況下,虛擬機需要在執(zhí)行過程中,根據(jù)調(diào)用者的動態(tài)類型,來確定具體的目標方法。
如虛擬機能確定目標方法有且僅有一個,比如說目標方法被標記為final[3][4],它可不通過動態(tài)類型,直接確定目標方法。
4.調(diào)用指令的符號引用
編譯過程中,我們并不知目標方法的具體內(nèi)存地址。因此,Java編譯器會暫時用符號引表示該目標方法。
這符號引用包括目標方法所在的類或接口的名字,以及目標方法的方法名和方法描述符。
符號引用存儲在class文件的常量池。根據(jù)目標方法是否為接口方法,這些引用可分為:
- 接口符號引用
- 非接口符號引用
// 在奸商.class的常量池中,#16為接口符號引用,指向接口方法"客戶.isVIP()"。#22為非接口符號引用,指向靜態(tài)方法"奸商.價格歧視()"。 $ javap -v 奸商.class ... Constant pool: ... ? #16 = InterfaceMethodref #27.#29 ? ? ? ?// 客戶.isVIP:()Z ... ? #22 = Methodref ? ? ? ? ?#1.#33 ? ? ? ? // 奸商.價格歧視:()D ...
執(zhí)行使用了符號引用的字節(jié)碼前,JVM需解析這些【符號引用】并替換為【實際引用】。
對【非接口符號引用】,假定該【符號引用】所指向的類為C,則JVM按如下步驟查找:
- 在C中查找符合名字及描述符的方法
- 若沒找到,搜索C的父類,直至Object類
- 若還沒找到,在C所直接實現(xiàn)或間接實現(xiàn)的接口中搜索,該步搜索得到的目標方法必須是非private、非static且若目標方法在間接實現(xiàn)的接口中,則需滿足C與該接口間無其他符合條件的目標方法。若有多個符合條件的目標方法,則返回其中任一。
所以static方法也可通過子類來調(diào)用。子類的static方法會隱藏(這不是重寫)父類中的同名、同描述符的靜態(tài)方法。
對于接口符號引用,假定該符號引用所指向的接口為I,則Java虛擬機會按照如下步驟進行查找。
- 在I中查找符合名字及描述符的方法。
- 如果沒有找到,在Object類中的公有實例方法中搜索。
- 如果沒有找到,則在I的超接口中搜索。這一步的搜索結(jié)果的要求與非接口符號引用步驟3的要求一致。
經(jīng)過上述解析步驟后,符號引用會被解析成實際引用:
- 對可靜態(tài)綁定的方法調(diào)用,實際引用是個指向方法的指針
- 對需動態(tài)綁定的方法調(diào)用,實際引用則是個方法表的索引
5.總結(jié)與實踐
文介紹了Java以及Java虛擬機是如何識別目標方法的。
在Java方法的:
- 重載,方法名相同而參數(shù)類型不相同的方法間
- 重寫,方法名相同&參數(shù)類型也相同的方法間
JVM識別方法的方式除了方法名和參數(shù)類型,還有返回類型。
JVM的:
- 靜態(tài)綁定:在解析時便能夠直接識別目標方法的情況
- 動態(tài)綁定,需在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標方法的情況。由于Java編譯器已區(qū)分重載方法,因此可認為JVM不存在重載
在class文件中,Java編譯器會用符號引用指代目標方法。在執(zhí)行調(diào)用指令前,它所附帶的符號引用需要被解析成實際引用。對于可以靜態(tài)綁定的方法調(diào)用而言,實際引用為目標方法的指針。對于需要動態(tài)綁定的方法調(diào)用而言,實際引用為輔助動態(tài)綁定的信息。
Java的重寫與Java虛擬機中的重寫并不一致,但編譯器會通過生成橋接方法來彌補。
到此這篇關(guān)于Java和JVM的重載識別,重寫方法是怎樣進行的的文章就介紹到這了,更多相關(guān)Java和JVM重載識別、重寫方法 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
參考:
[1] https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html
[2] https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
[3] https://wiki.openjdk.java.net/display/HotSpot/VirtualCalls
[4] https://wiki.openjdk.java.net/display/HotSpot/InterfaceCalls
相關(guān)文章
IntelliJ IDEA基于Scala實現(xiàn)Git檢查工具
這篇文章主要介紹了如何使用Scala實現(xiàn)自定義的Git檢查工具,大家可以基于本文的示例進行擴展與實現(xiàn),也可以進行其他應(yīng)用方向的嘗試,感興趣的可以了解下2023-08-08Java設(shè)計模式模板方法(Template)原理解析
這篇文章主要介紹了Java設(shè)計模式模板方法(Template)原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-11-11Mybatis單個參數(shù)的if判斷報異常There is no getter for property named ''x
今天小編就為大家分享一篇關(guān)于Mybatis單個參數(shù)的if判斷報異常There is no getter for property named 'xxx' in 'class java.lang.Integer'的解決方案,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12Idea中如何調(diào)出Run dashboard 或services窗口
這篇文章主要介紹了Idea中如何調(diào)出Run dashboard 或services窗口問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03