Java多態(tài)概述
多態(tài)是面向?qū)ο缶幊陶Z言的重要特性,它允許基類的指針或引用指向派生類的對象素挽,而在具體訪問時實現(xiàn)方法的動態(tài)綁定褐捻。Java 對于方法調(diào)用動態(tài)綁定的實現(xiàn)主要依賴于方法表渐尿,但通過類引用調(diào)用(invokevirtual)和接口引用調(diào)用(invokeinterface)的實現(xiàn)則有所不同。
類引用調(diào)用的大致過程為:Java編譯器將Java源代碼編譯成class文件捕犬,在編譯過程中跷坝,會根據(jù)靜態(tài)類型將調(diào)用的符號引用寫到class文件中。在執(zhí)行時碉碉,JVM根據(jù)class文件找到調(diào)用方法的符號引用柴钻,然后在靜態(tài)類型的方法表中找到偏移量,然后根據(jù)this指針確定對象的實際類型垢粮,使用實際類型的方法表贴届,偏移量跟靜態(tài)類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調(diào)用毫蚓,否則占键,認(rèn)為沒有重寫父類該方法。按照繼承關(guān)系從下往上搜索元潘。
接口引用調(diào)用后面再說吧畔乙。
從上圖可以看出,當(dāng)程序運行時翩概,需要某個類時牲距,類載入子系統(tǒng)會將相應(yīng)的class文件載入到JVM中,并在內(nèi)部建立該類的類型信息(這個類型信息其實就是class文件在JVM中存儲的一種數(shù)據(jù)結(jié)構(gòu))钥庇,包含java類定義的所有信息牍鞠,包括方法代碼,類變量评姨、成員變量难述、以及本博文要重點討論的方法表。這個類型信息就存儲在方法區(qū)吐句。
注意胁后,這個方法區(qū)中的類型信息跟在堆中存放的class對象是不同的。在方法區(qū)中蕴侧,這個class的類型信息只有唯一的實例(所以是各個線程共享的內(nèi)存區(qū)域)择同,而在堆中可以有多個該class對象两入【幌可以通過堆中的class對象訪問到方法區(qū)中類型信息。就像在java反射機制那樣裹纳,通過class對象可以訪問到該類的所有信息一樣择葡。
【重點】
方法表是實現(xiàn)動態(tài)調(diào)用的核心。上面講過方法表存放在方法區(qū)中的類型信息中剃氧。為了優(yōu)化對象調(diào)用方法的速度敏储,方法區(qū)的類型信息會增加一個指針,該指針指向一個記錄該類方法的方法表朋鞍,方法表中的每一個項都是對應(yīng)方法的指針已添。
這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。
【拓展】
方法區(qū):方法區(qū)和JAVA堆一樣滥酥,是各個線程共享的內(nèi)存區(qū)域更舞,用于存儲已被虛擬機加載的類信息、常量坎吻、靜態(tài)變量缆蝉、即時編譯器編譯后的代碼等數(shù)據(jù)。
運行時常量池:它是方法區(qū)的一部分,Class文件中除了有類的版本刊头、方法黍瞧、字段等描述信息外,還有一項信息是常量池原杂,用于存放編譯器生成的各種符號引用印颤,這部分信息在類加載時進入方法區(qū)的運行時常量池中。
方法區(qū)的內(nèi)存回收目標(biāo)是針對常量池的回收及對類型的卸載污尉。
Java 的方法調(diào)用方式
Java 的方法調(diào)用有兩類膀哲,動態(tài)方法調(diào)用與靜態(tài)方法調(diào)用。
- 靜態(tài)方法調(diào)用是指對于類的靜態(tài)方法的調(diào)用方式被碗,是靜態(tài)綁定的
- 動態(tài)方法調(diào)用需要有方法調(diào)用所作用的對象某宪,是動態(tài)綁定的。
類調(diào)用 (invokestatic) 是在編譯時就已經(jīng)確定好具體調(diào)用方法的情況锐朴。
實例調(diào)用 (invokevirtual)則是在調(diào)用的時候才確定具體的調(diào)用方法兴喂,這就是動態(tài)綁定,也是多態(tài)要解決的核心問題焚志。
JVM 的方法調(diào)用指令有四個衣迷,分別是 invokestatic,invokespecial酱酬,invokesvirtual 和 invokeinterface壶谒。前兩個是靜態(tài)綁定,后兩個是動態(tài)綁定的膳沽。本文也可以說是對于JVM后兩種調(diào)用實現(xiàn)的考察汗菜。
方法表與方法調(diào)用
如有類定義 Person, Girl, Boy
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() {
}
}
當(dāng)這三個類被載入到 Java 虛擬機之后,方法區(qū)中就包含了各自的類的信息挑社。Girl 和 Boy 在方法區(qū)中的方法表可表示如下:
可以看到陨界,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法痛阻。注意方法表條目指向的具體的方法地址菌瘪,如 Girl 繼承自 Object 的方法中,只有 toString() 指向自己的實現(xiàn)(Girl 的方法代碼)阱当,其余皆指向 Object 的方法代碼俏扩;其繼承自于 Person 的方法 eat() 和 speak() 分別指向 Person 的方法實現(xiàn)和本身的實現(xiàn)。
如果子類改寫了父類的方法弊添,那么子類和父類的那些同名的方法共享一個方法表項录淡。
因此,方法表的偏移量總是固定的表箭。所有繼承父類的子類的方法表中赁咙,其父類所定義的方法的偏移量也總是一個定值钮莲。
Person 或 Object中的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的彼水。這樣 JVM 在調(diào)用實例方法其實只需要指定調(diào)用方法表中的第幾個方法即可崔拥。
如調(diào)用如下:
class Party {
void happyHour() {
Person girl = new Girl();
girl.speak();
}
}
當(dāng)編譯 Party 類的時候,生成 girl.speak()的方法調(diào)用假設(shè)為:
Invokevirtual #12
設(shè)該調(diào)用代碼對應(yīng)著 girl.speak(); #12 是 Party 類的常量池的索引凤覆。JVM 執(zhí)行該調(diào)用指令的過程如下所示:
(1)在常量池(這里有個錯誤链瓦,上圖為ClassReference常量池而非Party的常量池)中找到方法調(diào)用的符號引用 。 (2)查看Person的方法表盯桦,得到speak方法在該方法表的偏移量(假設(shè)為15)慈俯,這樣就得到該方法的直接引用。
(3)根據(jù)this指針得到具體的對象(即 girl 所指向的位于堆中的對象)拥峦。
(4)根據(jù)對象得到該對象對應(yīng)的方法表贴膘,根據(jù)偏移量15查看有無重寫(override)該方法,如果重寫略号,則可以直接調(diào)用(Girl的方法表的speak項指向自身的方法而非父類)刑峡;如果沒有重寫,則需要拿到按照繼承關(guān)系從下往上的基類(這里是Person類)的方法表玄柠,同樣按照這個偏移量15查看有無該方法突梦。
接口調(diào)用
因為 Java 類是可以同時實現(xiàn)多個接口的,而當(dāng)用接口引用調(diào)用某個方法的時候羽利,情況就有所不同了宫患。
Java 允許一個類實現(xiàn)多個接口,從某種意義上來說相當(dāng)于多繼承这弧,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了
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
}
}
可以看到娃闲,由于接口的介入,繼承自于接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經(jīng)不一樣了当宴,顯然我們無法僅根據(jù)偏移量來進行方法的調(diào)用畜吊。
Java 對于接口方法的調(diào)用是采用搜索方法表的方式泽疆,如户矢,要在Dancer的方法表中找到dance()方法,必須搜索Dancer的整個方法表殉疼。
因為每次接口調(diào)用都要搜索方法表梯浪,所以從效率上來說,接口方法的調(diào)用總是慢于類方法的調(diào)用的瓢娜。
參考文章:
java動態(tài)綁定機制內(nèi)幕
深入理解java多態(tài)
Java多態(tài)實現(xiàn)原理