天天看點

Java多态實作原理

Java多态概述

多态是面向對象程式設計語言的重要特性,它允許基類的指針或引用指向派生類的對象,而在具體通路時實作方法的動态綁定。Java 對于方法調用動态綁定的實作主要依賴于方法表,但通過類引用調用(invokevitual)和接口引用調用(invokeinterface)的實作則有所不同。

類引用調用的大緻過程為:Java編譯器将Java源代碼編譯成class檔案,在編譯過程中,會根據靜态類型将調用的符号引用寫到class檔案中。在執行時,JVM根據class檔案找到調用方法的符号引用,然後在靜态類型的方法表中找到偏移量,然後根據this指針确定對象的實際類型,使用實際類型的方法表,偏移量跟靜态類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調用,否則,認為沒有重寫父類該方法。按照繼承關系從下往上搜尋。 

接口引用調用後面再說吧。

從上圖可以看出,當程式運作時,需要某個類時,類載入子系統會将相應的class檔案載入到JVM中,并在内部建立該類的類型資訊(這個類型資訊其實就是class檔案在JVM中存儲的一種資料結構),包含java類定義的所有資訊,包括方法代碼,類變量、成員變量、以及本博文要重點讨論的方法表。這個類型資訊就存儲在方法區。 

注意,這個方法區中的類型資訊跟在堆中存放的class對象是不同的。在方法區中,這個class的類型資訊隻有唯一的執行個體(是以是各個線程共享的記憶體區域),而在堆中可以有多個該class對象。可以通過堆中的class對象通路到方法區中類型資訊。就像在java反射機制那樣,通過class對象可以通路到該類的所有資訊一樣。

【重點】 

方法表是實作動态調用的核心。上面講過方法表存放在方法區中的類型資訊中。為了優化對象調用方法的速度,方法區的類型資訊會增加一個指針,該指針指向一個記錄該類方法的方法表,方法表中的每一個項都是對應方法的指針。

這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。

【拓展】

方法區:方法區和JAVA堆一樣,是各個線程共享的記憶體區域,用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。 

運作時常量池:它是方法區的一部分,Class檔案中除了有類的版本、方法、字段等描述資訊外,還有一項資訊是常量池,用于存放編譯器生成的各種符号引用,這部分資訊在類加載時進入方法區的運作時常量池中。 

方法區的記憶體回收目标是針對常量池的回收及對類型的解除安裝。

Java 的方法調用方式

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

靜态方法調用是指對于類的靜态方法的調用方式,是靜态綁定的

動态方法調用需要有方法調用所作用的對象,是動态綁定的。

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

執行個體調用 (invokevirtual)則是在調用的時候才确定具體的調用方法,這就是動态綁定,也是多态要解決的核心問題。

JVM 的方法調用指令有四個,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜态綁定,後兩個是動态綁定的。本文也可以說是對于JVM後兩種調用實作的考察。

方法表與方法調用

如有類定義 Person, Girl, Boy

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

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

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

是以,方法表的偏移量總是固定的。所有繼承父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。

Person 或 Object中的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在調用執行個體方法其實隻需要指定調用方法表中的第幾個方法即可。

如調用如下:

當編譯 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檢視有無該方法。

接口調用

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

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

可以看到,由于接口的介入,繼承自于接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經不一樣了,顯然我們無法僅根據偏移量來進行方法的調用。

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

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