1. 程式人生 > >Java技術——多型的實現原理

Java技術——多型的實現原理

0.前言

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

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

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

1Java多型概述

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

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


2.方法重寫後的動態繫結

多型允許具體訪問時實現方法的動態繫結。Java對於動態繫結的實現主要依賴於方法表,通過繼承和介面的多型實現有所不同。

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

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

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


從上圖可以看出,當程式執行需要某個類時,類載入器會將相應的class檔案載入到JVM中,並在方法區建立該類的型別資訊(包括方法程式碼,類變數、成員變數、以及本博文要重點討論的方法表)。 注意,這個方法區中的型別資訊跟在堆中存放的class物件是不同的。在方法區中,這個class的型別資訊只有唯一的例項(所以方法區是各個執行緒共享的記憶體區域),而在堆中可以有多個該class物件。可以通過堆中的class物件訪問到方法區中型別資訊。就像在java反射機制那樣,通過class物件可以訪問到該類的所有資訊一樣。

【重點】 

方法表是實現動態呼叫的核心。為了優化物件呼叫方法的速度,

方法區的型別資訊會增加一個指標,該指標指向記錄該類方法的方法表,方法表中的每一個項都是對應方法的指標。這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。

4Java 的方法呼叫方式(拓展知識,可以不看)

Java 的方法呼叫有兩類,動態方法呼叫與靜態方法呼叫。

靜態方法呼叫是指對於類的靜態方法的呼叫方式,是靜態繫結的;而動態方法呼叫需要有方法呼叫所作用的物件,是動態繫結的。

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

例項呼叫 (invokevirtual)則是在呼叫的時候才確定具體的呼叫方法,這就是動態繫結,也是多型要解決的核心問題

JVM 的方法呼叫指令有四個,分別是 invokestaticinvokespecialinvokesvirtual 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的整個方法表。

因為每次介面呼叫都要搜尋方法表,所以從效率上來說,介面方法的呼叫總是慢於類方法的呼叫的