本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看html
https://github.com/h2pl/Java-...
喜歡的話麻煩點下Star哈java
文章首發於個人我的博客:git
www.how2playlife.com
<!-- more -->程序員
Java 對於方法調用動態綁定的實現主要依賴於方法表,但經過類引用調用和接口引用調用的實現則有所不一樣。整體而言,當某個方法被調用時,JVM 首先要查找相應的常量池,獲得方法的符號引用,並查找調用類的方法表以肯定該方法的直接引用,最後才真正調用該方法。如下分別對該過程當中涉及到的相關部分作詳細介紹。github
典型的 Java 虛擬機的運行時結構以下圖所示面試
圖 1.JVM 運行時結構segmentfault
此結構中,咱們只探討和本文密切相關的方法區 (method area)。當程序運行須要某個類的定義時,載入子系統 (class loader subsystem) 裝入所需的 class 文件,並在內部創建該類的類型信息,這個類型信息就存貯在方法區。類型信息通常包括該類的方法代碼、類變量、成員變量的定義等等。能夠說,類型信息就是類的 Java 文件在運行時的內部結構,包含了改類的全部在 Java 文件中定義的信息。後端
注意到,該類型信息和 class 對象是不一樣的。class 對象是 JVM 在載入某個類後於堆 (heap) 中建立的表明該類的對象,能夠經過該 class 對象訪問到該類型信息。好比最典型的應用,在 Java 反射中應用 class 對象訪問到該類支持的全部方法,定義的成員變量等等。能夠想象,JVM 在類型信息和 class 對象中維護着它們彼此的引用以便互相訪問。二者的關係能夠類比於進程對象與真正的進程之間的關係。微信
Java 的方法調用有兩類,動態方法調用與靜態方法調用。靜態方法調用是指對於類的靜態方法的調用方式,是靜態綁定的;而動態方法調用須要有方法調用所做用的對象,是動態綁定的。類調用 (invokestatic) 是在編譯時刻就已經肯定好具體調用方法的狀況,而實例調用 (invokevirtual) 則是在調用的時候才肯定具體的調用方法,這就是動態綁定,也是多態要解決的核心問題。網絡
JVM 的方法調用指令有四個,分別是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜態綁定,後兩個是動態綁定的。本文也能夠說是對於 JVM 後兩種調用實現的考察。
常量池中保存的是一個 Java 類引用的一些常量信息,包含一些字符串常量及對於類的符號引用信息等。Java 代碼編譯生成的類文件中的常量池是靜態常量池,當類被載入到虛擬機內部的時候,在內存中產生類的常量池叫運行時常量池。
常量池在邏輯上能夠分紅多個表,每一個表包含一類的常量信息,本文只探討對於 Java 調用相關的常量池表。
CONSTANT_Utf8_info
字符串常量表,該表包含該類所使用的全部字符串常量,好比代碼中的字符串引用、引用的類名、方法的名字、其餘引用的類與方法的字符串描述等等。其他常量池表中所涉及到的任何常量字符串都被索引至該表。
CONSTANT_Class_info
類信息表,包含任何被引用的類或接口的符號引用,每個條目主要包含一個索引,指向 CONSTANT_Utf8_info 表,表示該類或接口的全限定名。
CONSTANT_NameAndType_info
名字類型表,包含引用的任意方法或字段的名稱和描述符信息在字符串常量表中的索引。
CONSTANT_InterfaceMethodref_info
接口方法引用表,包含引用的任何接口方法的描述信息,主要包括類信息索引和名字類型索引。
CONSTANT_Methodref_info
類方法引用表,包含引用的任何類型方法的描述信息,主要包括類信息索引和名字類型索引。
能夠看到,給定任意一個方法的索引,在常量池中找到對應的條目後,能夠獲得該方法的類索引(class_index)和名字類型索引 (name_and_type_index), 進而獲得該方法所屬的類型信息和名稱及描述符信息(參數,返回值等)。注意到全部的常量字符串都是存儲在 CONSTANT_Utf8_info 中供其餘表索引的。
方法表是動態調用的核心,也是 Java 實現動態調用的主要方式。它被存儲於方法區中的類型信息,包含有該類型所定義的全部方法及指向這些方法代碼的指針,注意這些具體的方法代碼多是被覆寫的方法,也多是繼承自基類的方法。
若有類定義 Person, Girl, Boy,
<pre name="code"> class Person {
public String toString(){
return "I'm a person."; }
public void eat(){}
public void speak(){}
}
class Boy extends Person{
public String toString(){
return "I'm a boy"; }
public void speak(){}
public void fight(){}
}
class Girl extends Person{
public String toString(){
return "I'm a girl"; }
public void speak(){}
public void sing(){}
}</pre>
當這三個類被載入到 Java 虛擬機以後,方法區中就包含了各自的類的信息。Girl 和 Boy 在方法區中的方法表可表示以下:
圖 3.Boy 和 Girl 的方法表
能夠看到,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表條目指向的具體的方法地址,如 Girl 的繼承自 Object 的方法中,只有 toString() 指向本身的實現(Girl 的方法代碼),其他皆指向 Object 的方法代碼;其繼承自於 Person 的方法 eat() 和 speak() 分別指向 Person 的方法實現和自己的實現。
Person 或 Object 的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是同樣的。這樣 JVM 在調用實例方法其實只須要指定調用方法表中的第幾個方法便可。
如調用以下:
清單 2
<pre name="code"> class Party{
…
void happyHour(){
Person girl = new Girl();
girl.speak();
…
}
}</pre>
當編譯 Party 類的時候,生成 girl.speak()
的方法調用假設爲:
Invokevirtual #12
設該調用代碼對應着 girl.speak(); #12 是 Party 類的常量池的索引。JVM 執行該調用指令的過程以下所示:
圖 4. 解析調用過程
JVM 首先查看 Party 的常量池索引爲 12 的條目(應爲 CONSTANT_Methodref_info 類型,可視爲方法調用的符號引用),進一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要調用的方法是 Person 的 speak 方法(注意引用 girl 是其基類 Person 類型),查看 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15(offset),這就是該方法調用的直接引用。
當解析出方法調用的直接引用後(方法表偏移量 15),JVM 執行真正的方法調用:根據實例方法調用的參數 this 獲得具體的對象(即 girl 所指向的位於堆中的對象),據此獲得該對象對應的方法表 (Girl 的方法表 ),進而調用方法表中的某個偏移量所指向的方法(Girl 的 speak() 方法的實現)。
由於 Java 類是能夠同時實現多個接口的,而當用接口引用調用某個方法的時候,狀況就有所不一樣了。Java 容許一個類實現多個接口,從某種意義上來講至關於多繼承,這樣一樣的方法在基類和派生類的方法表的位置就可能不同了。
清單 3
<pre name="code">interface IDance{
void dance();
}
class Person {
public String toString(){
return "I'm a person.";
}
public void eat(){}
public void speak(){}
}
class Dancer extends Person
implements IDance {
public String toString(){
return "I'm a dancer.";
}
public void dance(){}
}
class Snake implements IDance{
public String toString(){
return "A snake.";
}
public void dance(){
//snake dance
}
}</pre>
能夠看到,因爲接口的介入,繼承自於接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經不同了,顯然咱們沒法經過給出方法表的偏移量來正確調用 Dancer 和 Snake 的這個方法。這也是 Java 中調用接口方法有其專有的調用指令(invokeinterface)的緣由。
Java 對於接口方法的調用是採用搜索方法表的方式,對以下的方法調用
invokeinterface #13
JVM 首先查看常量池,肯定方法調用的符號引用(名稱、返回值等等),而後利用 this 指向的實例獲得該實例的方法表,進而搜索方法表來找到合適的方法地址。
由於每次接口調用都要搜索方法表,因此從效率上來講,接口方法的調用老是慢於類方法的調用的。
執行結果以下:
能夠看到System.out.println(dancer);
調用的是Person的toString方法。
Java 的繼承機制是一種複用類的技術,從原理上來講,是更好的使用了組合技術,所以要理解繼承,首先須要瞭解類的組合技術是如何實現類的複用的。
使用組合技術複用類
假設如今的需求是要建立一個具備基本類型,String 類型以及一個其餘非基本類型的對象。該如何處理呢?
對於基本類型的變量,在新類中成員變量處直接定義便可,但對於非基本類型變量,不只須要在類中聲明其引用,而且還須要手動初始化這個對象。
這裏須要注意的是,編譯器並不會默認將全部的引用都建立對象,由於這樣的話在不少狀況下會增長沒必要要的負擔,所以,在合適的時機初始化合適的對象,能夠經過如下幾個位置作初始化操做:
在定義對象的地方,先於構造方法執行。
在構造方法中。
在正要使用以前,這個被稱爲惰性初始化。
使用實例初始化。
class Soap { private String s; Soap() { System.out.println("Soap()"); s = "Constructed"; } public String tiString(){ return s; } } public class Bath { // s1 初始化先於構造函數 private String s1 = "Happy", s2 = "Happy", s3, s4; private Soap soap; private int i; private float f; public Both() { System.out.println("inSide Both"); s3 = "Joy"; f = 3.14f; soap = new Soap(); } { i = 88; } public String toString() { if(s4 == null){ s4 = "Joy" } return "s1 = " + s1 +"\n" + "s2 = " + s2 +"\n" + "s3 = " + s3 +"\n" + "s4 = " + s4 +"\n" + "i = " + i +"\n" + "f = " + f +"\n" + "soap = " + soap; } }
繼承
Java 中的繼承由 extend 關鍵字實現,組合的語法比較平實,而繼承是一種特殊的語法。當一個類繼承自另外一個類時,那麼這個類就能夠擁有另外一個類的域和方法。
class Cleanser{ private String s = "Cleanser"; public void append(String a){ s += a; } public void apply(){ append("apply"); } public void scrub(){ append("scrub"); } public String toString(){ return s; } public static void main(String args){ Cleanser c = new Cleanser(); c.apply(); System.out.println(c); } } public class Deter extends Cleanser{ public void apply(){ append("Deter.apply"); super.scrub(); } public void foam(){ append("foam"); } public static void main(String args){ Deter d = new Deter(); d.apply(); d.scrub(); d.foam(); System.out.println(d); Cleanser.main(args); } }
上面的代碼中,展現了繼承語法中的一些特性:
子類能夠直接使用父類中公共的方法和成員變量(一般爲了保護數據域,成員變量均爲私有)
子類中能夠覆蓋父類中的方法,也就是子類重寫了父類的方法,此時若還須要調用被覆蓋的父類的方法,則須要用到 super 來指定是調用父類中的方法。
子類中能夠自定義父類中沒有的方法。
能夠發現上面兩個類中均有 main 方法,命令行中調用的哪一個類就執行哪一個類的 main 方法,例如:java Deter。
繼承語法的原理
接下來咱們將經過建立子類對象來分析繼承語法在咱們看不到的地方作了什麼樣的操做。
能夠先思考一下,如何理解使用子類建立的對象呢,首先這個對象中包含子類的全部信息,可是也包含父類的全部公共的信息。
下面來看一段代碼,觀察一會兒類在建立對象初始化的時候,會不會用到父類相關的方法。
class Art{ Art() { System.out.println("Art Construct"); } } class Drawing extends Art { Drawing() { System.out.println("Drawing Construct"); } } public class Cartoon extends Drawing { public Cartoon() { System.out.println("Cartoon construct"); } public void static main(String args) { Cartoon c = new Cartoon(); } } /*output: Art Construct Drawing Construct Cartoon construct */
經過觀察代碼能夠發現,在實例化Cartoon時,事實上是從最頂層的父類開始向下逐個實例化,也就是最終實例化了三個對象。編譯器會默認在子類的構造方法中增長調用父類默認構造方法的代碼。
所以,繼承能夠理解爲編譯器幫咱們完成了類的特殊組合技術,即在子類中存在一個父類的對象,使得咱們能夠用子類對象調用父類的方法。而在開發者看來只不過是使用了一個關鍵字。
注意:雖然繼承很接近組合技術,可是繼承擁有其餘更多的區別於組合的特性,例如父類的對象咱們是不可見的,對於父類中的方法也作了相應的權限校驗等。
那麼,若是類中的構造方法是帶參的,該如何操做呢?(使用super關鍵字顯示調用)
見代碼:
class Game { Game(int i){ System.out.println("Game Construct"); } } class BoardGame extends Game { BoardGame(int j){ super(j); System.out.println("BoardGame Construct"); } } public class Chess extends BoardGame{ Chess(){ super(99); System.out.println("Chess construct"); } public static void main(String args) { Chess c = new Chess(); } } /*output: Game Construct BoardGame Construct Chess construc */
剛開始學習Java的時候,就瞭解了Java這個比較有意思的特性:重寫 和 重載。開始的有時候從名字上還老是容易弄混。我相信熟悉Java這門語言的同窗都應該瞭解這兩個特性,可能只是從語言層面上了解這種寫法,可是jvm是如何實現他們的呢 ?
重載官方給出的介紹:
一. overload:
The Java programming language supports overloading methods, and Java can distinguish between methods with different method signatures. This means that methods within a class can have the same name if they have different parameter lists .Overloaded methods are differentiated by the number and the type of the arguments passed into the method.
You cannot declare more than one method with the same name and the same number and type of arguments, because the compiler cannot tell them apart.
The compiler does not consider return type when differentiating methods, so you cannot declare two methods with the same signature even if they have a different return type.
首先看一段代碼,來看看代碼的執行結果:
public class OverrideTest { class Father{} class Sun extends Father {} public void doSomething(Father father){ System.out.println("Father do something"); } public void doSomething(Sun father){ System.out.println("Sun do something"); } public static void main(String [] args){ OverrideTest overrideTest = new OverrideTest(); Father sun = overrideTest.new Sun(); Father father = overrideTest.new Father(); overrideTest.doSomething(father); overrideTest.doSomething(sun); } }
看下這段代碼的執行結果,最後會打印:
Father do something
Father do something
爲何會打印出這樣的結果呢? 首先要介紹兩個概念:靜態分派和動態分派
靜態分派:依賴靜態類型來定位方法執行版本的分派動做稱爲靜態分派
動態分派:運行期根據實際類型肯定方法執行版本的分派過程。
他們的區別是:
1. 靜態分派發生在編譯期,動態分派發生在運行期;
2. private,static,final 方法發生在編譯期,而且不能被重寫,一旦發生了重寫,將會在運行期處理。
3. 重載是靜態分派,重寫是動態分派
回到上面的問題,由於重載是發生在編譯期,因此在編譯期已經肯定兩次 doSomething 方法的參數都是Father類型,在class文件中已經指向了Father類的符號引用,因此最後會打印兩次Father do something。
二. override:
An instance method in a subclass with the same signature (name, plus the number and the type of its parameters) and return type as an instance method in the superclass overrides the superclass's method.The ability of a subclass to override a method allows a class to inherit from a superclass whose behavior is "close enough" and then to modify behavior as needed. The overriding method has the same name, number and type of parameters, and return type as the method that it overrides. An overriding method can also return a subtype of the type returned by the overridden method. This subtype is called a covariant return type.
仍是上面那個代碼,稍微改動下
public class OverrideTest { class Father{} class Sun extends Father {} public void doSomething(){ System.out.println("Father do something"); } public void doSomething(){ System.out.println("Sun do something"); } public static void main(String [] args){ OverrideTest overrideTest = new OverrideTest(); Father sun = overrideTest.new Sun(); Father father = overrideTest.new Father(); overrideTest.doSomething(); overrideTest.doSomething(); } }
最後會打印:
Father do something
Sun do something
相信你們都會知道這個結果,那麼這個結果jvm是怎麼實現的呢?
在編譯期,只會識別到是調用Father類的doSomething方法,到運行期纔會真正找到對象的實際類型。
首先該方法的執行,jvm會調用invokevirtual指令,該指令會找棧頂第一個元素所指向的對象的實際類型,若是該類型存在調用的方法,則會走驗證流程,不然繼續找其父類。這也是爲何子類能夠直接調用父類具備訪問權限的方法的緣由。簡而言之,就是在運行期纔會去肯定對象的實際類型,根據這個實際類型肯定方法執行版本,這個過程稱爲動態分派。override 的實現依賴jvm的動態分派。
https://blog.csdn.net/dj_deng...
https://blog.csdn.net/chenssy...
https://blog.csdn.net/fan2012...
https://blog.csdn.net/fan2012...
https://www.cnblogs.com/seren...
https://blog.csdn.net/m0_3726...
若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!
Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。
做者是跨考軟件工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長爲阿里工程師。
做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。
黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!
關注公衆號【黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》
程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。