java多态的實作
Class 檔案的編譯過程中不包含傳統編譯中的連接配接步驟,一切方法調用在 Class 檔案裡面存儲的都隻是符号引用,而不是方法在實際運作時記憶體布局中的入口位址。這個特性給 Java 帶來了更強大的動态擴充能力,使得可以在類運作期間才能确定某些目标方法的直接引用,稱為動态連接配接,也有一部分方法的符号引用在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜态解析。
靜态解析成立的前提是:方法在程式真正執行前就有一個可确定的調用版本,并且這個方法的調用版本在運作期是不可改變的。換句話說,調用目标在編譯器進行編譯時就必須确定下來,這類方法的調用稱為解析。
在 Java 語言中,符合"編譯器可知,運作期不可變"這個要求的方法主要有靜态方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可被通路,這兩種方法都不可能通過繼承或别的方式重寫出其他的版本,是以它們都适合在類加載階段進行解析。
Java 虛拟機裡共提供了四條方法調用位元組指令,分别是:
• invokestatic:調用靜态方法。
• invokespecial:調用執行個體構造器方法、私有方法和父類方法。
• invokevirtual:調用所有的虛方法。
• invokeinterface:調用接口方法,會在運作時再确定一個實作此接口的對象。
隻要能被 invokestatic 和 invokespecial 指令調用的方法,都可以在解析階段确定唯一的調用版本,符合這個條件的有靜态方法、私有方法、執行個體構造器和父類方法四類,它們在類加載時就會把符号引用解析為該方法的直接引用。這些方法可以稱為非虛方法(還包括 final 方法),與之相反,其他方法就稱為虛方法(final 方法除外)。這裡要特别說明下 final 方法,雖然調用 final 方法使用的是 invokevirtual 指令,但是由于它無法覆寫,沒有其他版本,是以也無需對方發接收者進行多态選擇。Java 語言規範中明确說明了 final 方法是一種非虛方法。
如果定義Person,Gril,Boy類如下
當這三個了類被載入到Java虛拟機之後,方法區就包含了各自的類的資訊。Gril和Boy在方法區中的方法表可表示如下:
可以看到,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表條目指向的具體的方法位址,如 Girl 繼承自 Object 的方法中,隻有 toString() 指向自己的實作(Girl 的方法代碼),其餘皆指向 Object 的方法代碼;其繼承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法實作和本身的實作。
如果子類改寫了父類的方法,那麼子類和父類的那些同名的方法共享一個方法表項。
是以,方法表的偏移量總是固定的。所有繼承父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。
Person 或 Object中的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在調用執行個體方法其實隻需要指定調用方法表中的第幾個方法即可。
如調用如下:
當編譯Person類的時候,生成girl.speak()的方法調用假設為:
Invokevirtual #12
設該調用代碼對應着 girl.speak(); #12 是 Person類的常量池的索引。JVM 執行該調用指令的過程如下所示:
(1)在常量池中找到方法調用的符号引用 。
(2)檢視Person的方法表,得到speak方法在該方法表的偏移量(假設為15),這樣就得到該方法的直接引用。
(3)根據this指針得到具體的對象(即 girl 所指向的位于堆中的對象)。
(4)根據對象得到該對象對應的方法表,根據偏移量15檢視有無重寫(override)該方法,如果重寫,則可以直接調用(Girl的方法表的speak項指向自身的方法而非父類);如果沒有重寫,則需要拿到按照繼承關系從下往上的基類(這裡是Person類)的方法表,同樣按照這個偏移量15檢視有無該方法。