1. 程式人生 > >反射和多型實現原理詳解

反射和多型實現原理詳解

Table of Contents

反射和多型

多型

多型的定義和用法

多型的實現原理

反射

反射的實現原理

反射的應用

反射的弊端


反射和多型

這兩種技術並無直接聯絡,之所以把它們放在一起說,是因為Java提供讓我們在執行時識別物件和類的資訊,主要有2種方式:一種是傳統的RTTI,它假定我們在編譯時已經知道了所有的型別資訊,這是多型的實現基礎;另一種是反射機制,它允許我們在執行時發現和使用類的資訊。

多型

首先明確一點我們只考慮執行時多型,而不考慮編譯時多型(方法過載)。和反射同為執行時獲取程式資訊,讓我們先來看一下多型。另外,多型的技術帶來的影響之一:由於一個藉口可能有多個實現,而每個實現之間的大小,規模,是不一樣的。因此多型對記憶體的分配是有影響的,不同的實現會有不同的記憶體分配

多型的定義和用法

多型通常有兩種實現方法:

  1. 子類繼承父類(extends)
  2. 類實現介面(implements)

核心之處就在於對父類方法的改寫或對介面方法的實現,以取得在執行時不同的執行效果。要使用多型,在宣告物件時就應該遵循一條法則:宣告的總是父類型別或介面型別,建立的是實際型別. 

以ArrayList為例子,要使用多型的特性,要按照如下方式定義

List list = new ArrayList();

此外,在定義方法引數時也通常總是應該優先使用父類型別或介面型別,例如:

public void test(List list);

這樣宣告最大的好處在於它的靈活性,假如某一天ArrayList無法滿足要求,我們希望用LinkedList來代替它,那麼只需要在物件建立的地方把new ArrayList()改為new LinkedList即可,其它程式碼一概不用改動。

多型的實現原理

RTTI,即Run-Time Type Identification,通過執行時型別資訊程式能夠使用基類指標或引用來檢查這些指標或引用所指的物件的實際派生類型。RTTI的功能主要是通過Class類實現的。

Class類是"類的類"(class of classes)。如果說類是物件的抽象的話,那麼Class類就是對類的抽象。

每一個Class類的物件代表一個特定的類。請看如下程式碼

import java.lang.Class;

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Unicycle unicycle = new Unicycle("Unicycle");
        Cycle.ride(unicycle);
        Class c1 = unicycle.getClass();
        System.out.println(c1.getName());

        Bicycle bicycle = new Bicycle("Bicycle");
        Cycle.ride(bicycle);
        Class c2 = Class.forName("com.dr.Basic.Bicycle");
        System.out.println(c2.getName());

        Tricycle tricycle = new Tricycle("Tricycle");
        Cycle.ride(tricycle);
        Class c3      = Tricycle.class;
        System.out.println(c3.getName());
    }
}

class Cycle {
    private String name;

    public Cycle(String str) {
        name = str;
    }

    public static void ride(Cycle c) {
        System.out.println(c.name + "is riding");
    }
}

class Unicycle extends Cycle {
    private String name;

    public Unicycle(String str) {
        super(str);
        name = str;
    }
}

class Bicycle extends Cycle {
    private String name;

    public Bicycle(String str) {
        super(str);
        name = str;
    }
}

class Tricycle extends Cycle {
    private String name;

    public Tricycle(String str) {
        super(str);
        name = str;
    }
}

這是一個普通的多型的示例程式,但是我在每一處多型呼叫時,分別去獲取了他們的class物件並打印出來。列印結果如下:


com.dr.Basic.Unicycle

com.dr.Basic.Bicycle

com.dr.Basic.Tricycle

可以發現即使我們將物件的引用向上轉型,物件所指向的Class類物件依然是實際的實現類。

Java中每個物件都有相應的Class類物件,因此,我們隨時能通過Class物件知道某個物件“真正”所屬的類。無論我們對引用進行怎樣的型別轉換,物件本身所對應的Class物件都是同一個。這意味著java在執行時的確能確定真正的實現類是哪一個。

下面從虛擬機器執行時的角度來簡要介紹多型的實現原理,這裡以Java虛擬機器(Java Virtual Machine, JVM)規範的實現為例。

在JVM執行Java位元組碼時,型別資訊被存放在方法區中,通常為了優化物件呼叫方法的速度,方法區的型別資訊中增加一個指標,該指標指向一張記錄該類方法入口的表(稱為方法表),表中的每一項都是指向相應方法的指標。

方法表的構造如下:

由於Java的單繼承機制,一個類只能繼承一個父類,而所有的類又都繼承自Object類。方法表中最先存放的是Object類的方法,接下來是該類的父類的方法,最後是該類本身的方法。這裡關鍵的地方在於如果子類改寫了父類的方法,那麼子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法

注意這裡只有非私有的例項方法才會出現,並且靜態方法也不會出現在這裡,原因很容易理解:靜態方法跟物件無關,可以將方法地址直接引用,而不像例項方法需要間接引用。

更深入地講,靜態方法是由虛擬機器指令invokestatic呼叫的,私有方法和建構函式則是由invokespecial指令呼叫,只有被invokevirtual和invokeinterface指令呼叫的方法才會在方法表中出現。

由於以上方法的排列特性(Object——父類——子類),使得方法表的偏移量總是固定的。例如,對於任何類來說,其方法表中equals方法的偏移量總是一個定值,所有繼承某父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。

前面說過,方法表中的表項都是指向該類對應方法的指標,這裡就開始了多型的實現:

假設Class A是Class B的子類,並且A改寫了B的方法method(),那麼在B的方法表中,method方法的指標指向的就是B的method方法入口。

而對於A來說,它的方法表中的method方法則會指向其自身的method方法而非其父類的(這在類載入器載入該類時已經保證,同時JVM會保證總是能從物件引用指向正確的型別資訊)。

結合方法指標偏移量是固定的以及指標總是指向實際類的方法域,我們不難發現多型的機制就在這裡:

在呼叫方法時,實際上必須首先完成例項方法的符號引用解析,結果是該符號引用被解析為方法表的偏移量。虛擬機器通過物件引用得到方法區中型別資訊的入口,查詢類的方法表,當將子類物件宣告為父類型別時,形式上呼叫的是父類方法,此時虛擬機器會從實際類的方法表(雖然宣告的是父類,但是實際上這裡的型別資訊中存放的是子類的資訊)中查詢該方法名對應的指標(這裡用“查詢”實際上是不合適的,前面提到過,方法的偏移量是固定的,所以只需根據偏移量就能獲得指標),進而就能指向實際類的方法了。

我們的故事還沒有結束,事實上上面的過程僅僅是利用繼承實現多型的內部機制,多型的另外一種實現方式:實現介面相比而言就更加複雜,原因在於,Java的單繼承保證了類的線性關係,而介面可以同時實現多個,這樣光憑偏移量就很難準確獲得方法的指標。所以在JVM中,多型的例項方法呼叫實際上有兩種指令:

invokevirtual指令用於呼叫宣告為類的方法;

invokeinterface指令用於呼叫宣告為介面的方法。

當使用invokeinterface指令呼叫方法時,就不能採用固定偏移量的辦法,只能老老實實挨個找了(當然實際實現並不一定如此,JVM規範並沒有規定究竟如何實現這種查詢,不同的JVM實現可以有不同的優化演算法來提高搜尋效率)。我們不難看出,在效能上,呼叫介面引用的方法通常總是比呼叫類的引用的方法要慢。這也告訴我們,在類和介面之間優先選擇介面作為設計並不總是正確的,當然設計問題不在本文探討的範圍之內,但顯然具體問題具體分析仍然不失為更好的選擇。

這就是多型的原理。總結起來說就是兩點:

1.是方法表起了決定性作用,如果子類改寫了父類的方法,那麼子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法。

2.類和介面的多型實現不一樣,類的可以使用固定偏移,但介面只能挨個找,原因是介面的實現不是確定唯一的。

 

反射

反射的定義如下:java程式在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠調用它的任意方法和屬性這種動態獲取資訊以及動態呼叫物件方法的功能稱為java語言的反射機制。

反射的實現原理

如果不知道某個物件的確切型別(即list引用到底是ArrayList型別還是LinkedList型別),RTTI可以告訴你,但是有一個前提:這個型別在編譯時必須已知,這樣才能使用RTTI來識別它。

想理解反射的原理,必須要結合類載入機。反射機制並沒有什麼神奇之處,當通過反射與一個未知型別的物件打交道時,JVM只是簡單地檢查這個物件,看它屬於哪個特定的類。因此,那個類的.class對於JVM來說必須是可獲取的,要麼在本地機器上,要麼從網路獲取。所以對於RTTI和反射之間的真正區別只在於:

  • RTTI,編譯器在編譯時開啟和檢查.class檔案
  • 反射,執行時開啟和檢查.class檔案

總結起來說就是,反射是通過Class類和java.lang.reflect類庫一起支援而實現的,其中每一個Class類的物件都對應了一個類,這些資訊在編譯時期就已經被存在了.class檔案裡面了,Class 物件是在載入類時由 Java 虛擬機器以及通過呼叫類載入器中的defineClass方法自動構造的。也就是這不需要我們自己去處理建立,JVM已經幫我們建立好了。對於我們定義的每一個類,在虛擬機器中都有一個應的Class物件。

那麼在執行時期,無論是通過字面量還是forName方法獲取Class物件,都是去根據這個類的全限定名(全限定名必須是唯一的,這也間接回答了為什麼類名不能重複這個問題。)然後獲取對應的Class物件

Class類提供了獲取getFields()、getMethods()和getConstructors()等方法,而這些方法的返回值型別就定義在java.lang.reflect當中。

總結: 通過類的全限定名,去獲取這個類的位元組碼.class檔案,然後再獲取這個類對應的class物件,再通過class物件提供的方法結合類Method,Filed,Constructor,就能獲取到這個類的所有相關資訊

反射的應用

非常之重要應用也非常之廣泛,比較出名的有

1. Spring/Mybatis等框架,行內有一句這樣的老話:反射機制是Java框架的基石。最經典的就是xml的配置模式。

2. JDBC 的資料庫的連線

3. 動態生成物件,應用於工廠模式中. spring的bean容器也就是一個工廠

4. jdk動態代理

5. 註解機制的實現

   利用反射可以獲取每一個filed,Filed類提供了getDeclaredAnnotations方法以陣列形式返回這個欄位所有的註解....

6. 編輯器程式碼自動提示的實現

...

反射的弊端

1.效能

反射包括了一些動態型別,所以 JVM 無法對這些程式碼進行優化。因此,反射操作的效率要比那些非反射操作低得多。我們應該避免在經常被 執行的程式碼或對效能要求很高的程式中使用反射。

2. 安全

使用反射技術要求程式必須在一個沒有安全限制的環境中執行。如果一個程式必須在有安全限制的環境中執行,如 Applet,那麼這就是個問題了。

3. 內部暴露

由於反射允許程式碼執行一些在正常情況下不被允許的操作(比如訪問私有的屬性和方法),所以使用反射可能會導致意料之外的副作用--程式碼有功能上的錯誤,降低可移植性。反射程式碼破壞了抽象性,因此當平臺發生改變的時候,程式碼的行為就有可能也隨著變化。

4.喪失了編譯時型別檢查的好處,包括異常檢查。如果程式企圖用反射去呼叫不存在或者不可訪問方法,在執行時將會失敗。

5.從程式碼規範的角度來說,執行反射訪問所需要的程式碼非常笨拙和冗長。這樣的程式碼閱讀起來很困難

核心反射機制最初是為了基於元件的應用建立工具而設計的,如spring。這類工具通常需要裝載類,並且用反射功能找出它們支援哪些方法和構造器。這些工具允許使用者互動式的構建訪問這些類的應用程式。

反射功能只是在設計時design time被用到,通常,普通應用程式執行時不應該以反射方式訪問物件。對於特定的複雜系統程式設計任務,它是非常必要的,但它也有一些缺點,如果你編寫的程式必須要與編譯時未知的類一起工作,如有可能就應該僅僅使用反射機制來例項化物件,而訪問物件時則使用編譯時已知的某個介面或者超類。

基於此,在effective java也總結了一條介面優先於反射機制的開發原則。

反射相關類實現原理

 

Field類

 

Method類