Java多態的實現原理


0.前言

轉載請標明出處:http://blog.csdn.net/seu_calvin/article/details/52191321

多態在Java技術里有很重要的地位,在面試中也會經常被問到。

多態的使用大家應該都比較了解,但是多態的實現原理就有點抽象了,查了很多很多資料,連續幾天斷斷續續的看,有時候看着看着就走神了。畢竟太抽象,哈哈~

不過依然硬着頭皮看下來了(也不知道看了多少遍),並且將很多資料里關於多態的知識進行了整理(添添加加刪刪減減了很久,也把重點根據自己的理解用紅字標出),便有了這篇文章。通過這篇文章相信可以幫助你更加深刻的理解多態。

 

1.Java多態概述

 

Java的方法重載,就是在類中可以創建多個方法,它們具有相同的名字,但可具有不同的參數列表、返回值類型。調用方法時通過傳遞的參數類型來決定具體使用哪個方法,這就是多態性。

Java的方法重寫,是父類與子類之間的多態性,子類可繼承父類中的方法,但有時子類並不想原封不動地繼承父類的方法,而是想作一定的修改,這就需要采用方法的重寫。重寫的參數列表和返回類型均不可修改。

 

2.方法重寫后的動態綁定

多態允許具體訪問時實現方法的動態綁定。Java對於動態綁定的實現主要依賴於方法表,通過繼承和接口的多態實現有所不同。

繼承:在執行某個方法時,在方法區中找到該類的方法表,再確認該方法在方法表中的偏移量,找到該方法后如果被重寫則直接調用,否則認為沒有重寫父類該方法,這時會按照繼承關系搜索父類的方法表中該偏移量對應的方法。 

接口:Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同一個接口的的方法在不同類方法表中的位置就可能不一樣了。所以不能通過偏移量的方法,而是通過搜索完整的方法表。

 

3.JVM的結構(拓展知識,不了解可以看看)

 

 

從上圖可以看出,當程序運行需要某個類時,類加載器會將相應的class文件載入到JVM中,並在方法區建立該類的類型信息(包括方法代碼,類變量、成員變量、以及本博文要重點討論的方法表)。 
注意,這個方法區中的類型信息跟在堆中存放的class對象是不同的。在方法區中,這個class的類型信息只有唯一的實例(所以方法區是各個線程共享的內存區域),而在堆中可以有多個該class對象。可以通過堆中的class對象訪問到方法區中類型信息。就像在java反射機制那樣,通過class對象可以訪問到該類的所有信息一樣。

【重點】 

方法表是實現動態調用的核心。為了優化對象調用方法的速度,方法區的類型信息會增加一個指針,該指針指向記錄該類方法的方法表,方法表中的每一個項都是對應方法的指針。這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。

 

4.Java 的方法調用方式(拓展知識,可以不看)

Java 的方法調用有兩類,動態方法調用與靜態方法調用。

靜態方法調用是指對於類的靜態方法的調用方式,是靜態綁定的;而動態方法調用需要有方法調用所作用的對象,是動態綁定的。

類調用 (invokestatic) 是在編譯時就已經確定好具體調用方法的情況。

實例調用 (invokevirtual)則是在調用的時候才確定具體的調用方法,這就是動態綁定,也是多態要解決的核心問題。

JVM 的方法調用指令有四個,分別是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜態綁定,后兩個是動態綁定的。本文也可以說是對於JVM后兩種調用實現的考察。

 

 

5.方法表與方法調用

 

如有類定義 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(){}
}

 

當這三個類被載入到 Java 虛擬機之后,方法區中就包含了各自的類的信息。Girl 和 Boy 在方法區中的方法表可表示如下:



可以看到,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表條目指向的具體的方法地址,如 Girl 繼承自 Object 的方法中,只有 toString() 指向自己的實現(Girl 的方法代碼),其余皆指向 Object 的方法代碼;其繼承自於 Person 的方法 eat() 和 speak() 分別指向 Person 的方法實現和本身的實現。

如果子類改寫了父類的方法,那么子類和父類的那些同名的方法共享一個方法表項。

因此,方法表的偏移量總是固定的。所有繼承父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。
Person 或 Object中的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在調用實例方法其實只需要指定調用方法表中的第幾個方法即可。

如調用如下:

 

class Party{
void happyHour(){
Person girl = new Girl();
girl.speak(); }
}

 

當編譯 Party 類的時候,生成 girl.speak()的方法調用假設為:

Invokevirtual #12

設該調用代碼對應着 girl.speak(); #12 是 Party 類的常量池的索引。JVM 執行該調用指令的過程如下所示:

 

 

(1)在常量池(這里有個錯誤,上圖為ClassReference常量池而非Party的常量池)中找到方法調用的符號引用 。
(2)查看Person的方法表,得到speak方法在該方法表的偏移量(假設為15),這樣就得到該方法的直接引用。 
(3)根據this指針得到具體的對象(即 girl 所指向的位於堆中的對象)。
(4)根據對象得到該對象對應的方法表,根據偏移量15查看有無重寫(override)該方法,如果重寫,則可以直接調用(Girl的方法表的speak項指向自身的方法而非父類);如果沒有重寫,則需要拿到按照繼承關系從下往上的基類(這里是Person類)的方法表,同樣按照這個偏移量15查看有無該方法。

 

6.接口調用

 

因為 Java 類是可以同時實現多個接口的,而當用接口引用調用某個方法的時候,情況就有所不同了。

Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。

 

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 的方法表中的位置已經不一樣了,顯然我們無法僅根據偏移量來進行方法的調用。

Java 對於接口方法的調用是采用搜索方法表的方式,如,要在Dancer的方法表中找到dance()方法,必須搜索Dancer的整個方法表。

因為每次接口調用都要搜索方法表,所以從效率上來說,接口方法的調用總是慢於類方法的調用的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM