認(rèn)識(shí)一下Java中方法重載和重寫的“真面目”
前言
考大家一道題目,下面的類執(zhí)行結(jié)果是什么???
public class DispatcherClient {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
不知道大家心里的答案是什么?反正我的答案是錯(cuò)的。
正確的答案是:
為什么是Animal Animal Animal? 而不是Animal dog cat。
類重載本質(zhì)——靜態(tài)分派
execute方法是一個(gè)重載方法,本質(zhì)上就是虛擬機(jī)JVM如何確定調(diào)用哪個(gè)方法執(zhí)行。在java編譯后的class文件中存儲(chǔ)的只是方法的符號(hào)引用,而不是方法在實(shí)際運(yùn)行過(guò)程中內(nèi)存布局的入口地址(直接引用)。而這個(gè)方法從符號(hào)引用變成直接引用有兩種方式,解析和分派。
解析是發(fā)生在類加載的解析階段就會(huì)將一部分方法的符號(hào)引用轉(zhuǎn)換為直接引用,比如類的靜態(tài)方法、私有方法、構(gòu)造方法、父類方法以及final的方法。我們這里不展開闡述,和本例無(wú)關(guān)。
而我們方法重載的情況下,java采用的是靜態(tài)分派的方式確定調(diào)用方法。
變量類型
在了解靜態(tài)分派前我們需要了解下變量的類型。
Animal a1 = new Dog();
- 靜態(tài)類型, 也叫做"外觀類型", 比如代碼中的"Animal", 它的類型是在編譯期就知道。
- 實(shí)際類型,也叫"運(yùn)行時(shí)類型", 比如代碼中的"Dog", 它是在類運(yùn)行時(shí)才會(huì)確定,編譯期是不知道的。
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
這里多次調(diào)用了execute方法,在方法接收者已經(jīng)確定是對(duì)象exe的前提下,使用哪個(gè)重載的方法,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型。虛擬機(jī)在重載時(shí)是通過(guò)參數(shù)的靜態(tài)類型而不是實(shí)際類型作為判斷依據(jù)的。因?yàn)殪o態(tài)類型是編譯期可知的,所以,在編譯階段,編譯器會(huì)根據(jù)靜態(tài)類型決定使用哪個(gè)重載版本,如下圖例子中的字節(jié)碼,技術(shù)在編譯的字節(jié)碼中確定了它調(diào)用的重載方法。
類多態(tài)本質(zhì)——?jiǎng)討B(tài)分派
既然有靜態(tài)分派,那么是不是有動(dòng)態(tài)分派呢?什么又是動(dòng)態(tài)派呢?
Java語(yǔ)言的一大特性是多態(tài)性,所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過(guò)該引用變量發(fā)出的方法調(diào)用在編程時(shí)并不確定,而是在程序運(yùn)行期間才確定,即一個(gè)引用變量倒底會(huì)指向哪個(gè)類的實(shí)例對(duì)象,該引用變量發(fā)出的方法調(diào)用到底是哪個(gè)類中實(shí)現(xiàn)的方法,必須在由程序運(yùn)行期間才能決定。
舉個(gè)簡(jiǎn)單的例子,比如Human human = flag ? new Man() : new Woman(), human的具體類型是man還是woman在編寫代碼的時(shí)候我們是無(wú)法確定,它是由flag這個(gè)標(biāo)記決定,只有在程序運(yùn)行的時(shí)候才能夠確定下來(lái),這種讓引用變量在運(yùn)行時(shí)綁定到各種不同的類實(shí)現(xiàn)上,從而導(dǎo)致該引用調(diào)用的具體方法隨之改變,即不修改程序代碼就可以改變程序運(yùn)行時(shí)所綁定的具體代碼,讓程序可以選擇多個(gè)運(yùn)行狀態(tài),這就是多態(tài)性。
多態(tài)在Java中有兩種實(shí)現(xiàn)形式,分別是繼承和接口,子類重寫父類或者接口中的方法,現(xiàn)在舉個(gè)例子。
public class DynamicDispatch {
static abstract class Animal {
protected abstract void eat();
}
static class Cat extends Animal {
@Override
protected void eat() {
System.out.println("我吃魚");
}
}
static class Dog extends Animal {
@Override
protected void eat() {
System.out.println("我吃骨頭");
}
}
public static void main(String[] args) {
Animal cat = new Cat();
Animal dog = new Dog();
cat.eat();
dog.eat();
cat = new Dog();
cat.eat();
}
}
運(yùn)行結(jié)果:
這個(gè)結(jié)果相信和大家想的是一致的,那大家有想過(guò)JVM是怎么找到具體的類型執(zhí)行的呢?我們定義的引用類型就是Animal,JVM是根據(jù)什么來(lái)找到對(duì)應(yīng)的Cat 或者Dog這些具體的實(shí)例執(zhí)行對(duì)應(yīng)的方法呢?
從字節(jié)碼角度分析
利用idea的Jclasslib插件查看字節(jié)碼:
- 0~15行主要是創(chuàng)建Cat對(duì)象和Dog對(duì)象的字節(jié)碼指令。
- 17和21行一模一樣,指令都是invokevirtual, 參數(shù)都是<com/alvin/chapter8/DynamicDispatch$Animal.eat。竟然這兩條指令一模一樣,那他是怎么確定調(diào)用哪個(gè)實(shí)際類型的方法呢?這還得要了解invokevirtual指令的運(yùn)行過(guò)程:
- 找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象作為實(shí)際類型,記作類型C,這個(gè)是在運(yùn)行期確定的。
- 如果在類型C中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問(wèn)權(quán)限校驗(yàn),通過(guò)返回這個(gè)方法的直接引用,查過(guò)過(guò)程結(jié)束。
- 否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類進(jìn)行搜索和驗(yàn)證。
- 如果始終沒(méi)有找到合適的方法,拋出AbstractMethodError異常。
- 回過(guò)頭來(lái)看,我們看到字節(jié)碼中的第16行和20行的aload指令就是把剛剛創(chuàng)建的對(duì)象壓入到棧頂。
以上的過(guò)程中根據(jù)方法接收者的實(shí)際類型來(lái)確定調(diào)用那個(gè)方法,找不到往父類繼續(xù)找的過(guò)程,其實(shí)也就是重寫的本質(zhì)。我們把這種在運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的分派過(guò)程叫做動(dòng)態(tài)分派。
** 虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn) **
上面講述了虛擬即動(dòng)態(tài)分派的過(guò)程,那它是怎么實(shí)現(xiàn)這一過(guò)程的呢?
因?yàn)閯?dòng)態(tài)分派是執(zhí)行非常頻繁的動(dòng)作,而且需要在運(yùn)行時(shí)搜索合適的目標(biāo)方法,基于性能的考慮,java虛擬機(jī)采用了一種基礎(chǔ)且常見(jiàn)的優(yōu)化手段—為類型在方法區(qū)建立一個(gè)需方法表。使用需方法表索引來(lái)代替元數(shù)據(jù)查找以提高性能。
虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類中沒(méi)有被重寫,那子類的虛方法表中的地址入口和父類相同方法的地址入口時(shí)一致的,如果子類重寫了方法,子類虛方法表中的地址會(huì)被替換為指向子類實(shí)現(xiàn)版本的入口地址。
總結(jié)
總結(jié)下,所有依賴靜態(tài)類型來(lái)定位方法執(zhí)行版本的分派叫做靜態(tài)分派。靜態(tài)分派的典型應(yīng)用就是方法重載,它是在編譯階段確定的,它會(huì)選擇一個(gè)最合適的版本方法進(jìn)行調(diào)用。而動(dòng)態(tài)分派簡(jiǎn)單來(lái)說(shuō)就是根據(jù)變量的動(dòng)態(tài)類型確定執(zhí)行哪個(gè)方法,典型的應(yīng)用就是方法的重寫。