天天看點

[轉載]Java中的類反射機制

一、反射的概念:

反射的概念是由Smith在1982年首次提出的,主要是指程式可以通路、檢測和修改它本身狀态或行為的一種能力。這一概念的提出很快引發了計算機科學領域關于應用反射性的研究。它首先被程式語言的設計領域所采用,并在Lisp和面向對象方面取得了成績。其中LEAD/LEAD++ 、OpenC++ 、MetaXa和OpenJava等就是基于反射機制的語言。最近,反射機制也被應用到了視窗系統、作業系統和檔案系統中。

反射本身并不是一個新概念,它可能會使我們聯想到光學中的反射概念,盡管計算機科學賦予了反射概念新的含義,但是,從現象上來說,它們确實有某些相通之處,這些有助于我們的了解。在計算機科學領域,反射是指一類應用,它們能夠自描述和自控制。也就是說,這類應用通過采用某種機制來實作對自己行為的描述(self-representation)和監測(examination),并能根據自身行為的狀态和結果,調整或修改應用所描述行為的狀态和相關的語義。可以看出,同一般的反射概念相比,計算機科學領域的反射不單單指反射本身,還包括對反射結果所采取的措施。所有采用反射機制的系統(即反射系統)都希望使系統的實作更開放。可以說,實作了反射機制的系統都具有開放性,但具有開放性的系統并不一定采用了反射機制,開放性是反射系統的必要條件。一般來說,反射系統除了滿足開放性條件外還必須滿足原因連接配接(Causally-connected)。所謂原因連接配接是指對反射系統自描述的改變能夠立即反映到系統底層的實際狀态和行為上的情況,反之亦然。開放性和原因連接配接是反射系統的兩大基本要素。13700863760

Java中,反射是一種強大的工具。它使您能夠建立靈活的代碼,這些代碼可以在運作時裝配,無需在元件之間進行源代表連結。反射允許我們在編寫與執行時,使我們的程式代碼能夠接入裝載到JVM中的類的内部資訊,而不是源代碼中標明的類協作的代碼。這使反射成為建構靈活的應用的主要工具。但需注意的是:如果使用不當,反射的成本很高。

二、Java中的類反射:

Reflection 是 Java 程式開發語言的特征之一,它允許運作中的 Java 程式對自身進行檢查,或者說“自審”,并能直接操作程式的内部屬性。Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程式設計語言中根本就不存在這一特性。例如,Pascal、C 或者 C++ 中就沒有辦法在程式中獲得函數定義相關的資訊。

1.檢測類:

1.1 reflection的工作機制

考慮下面這個簡單的例子,讓我們看看 reflection 是如何工作的。

import java.lang.reflect.*;

public class DumpMethods {

    public static void main(String args[]) {

        try {

            Class c = Class.forName(args[0]);

            Method m[] = c.getDeclaredMethods();

            for (int i = 0; i < m.length; i++)

                System.out.println(m[i].toString());

        } catch (Throwable e) {

            System.err.println(e);

        }

    }

}

按如下語句執行:

java DumpMethods java.util.Stack

它的結果輸出為:

public java.lang.Object java.util.Stack.push(java.lang.Object)

public synchronized java.lang.Object java.util.Stack.pop()

public synchronized java.lang.Object java.util.Stack.peek()

public boolean java.util.Stack.empty()

public synchronized int java.util.Stack.search(java.lang.Object)

這樣就列出了java.util.Stack 類的各方法名以及它們的限制符和傳回類型。

這個程式使用 Class.forName 載入指定的類,然後調用 getDeclaredMethods 來擷取這個類中定義了的方法清單。java.lang.reflect.Methods 是用來描述某個類中單個方法的一個類。

1.2 Java類反射中的主要方法

對于以下三類元件中的任何一類來說 -- 構造函數、字段和方法 -- java.lang.Class 提供四種獨立的反射調用,以不同的方式來獲得資訊。調用都遵循一種标準格式。以下是用于查找構造函數的一組反射調用:

l         Constructor getConstructor(Class[] params) -- 獲得使用特殊的參數類型的公共構造函數,

l         Constructor[] getConstructors() -- 獲得類的所有公共構造函數

l         Constructor getDeclaredConstructor(Class[] params) -- 獲得使用特定參數類型的構造函數(與接入級别無關)

l         Constructor[] getDeclaredConstructors() -- 獲得類的所有構造函數(與接入級别無關)

獲得字段資訊的Class 反射調用不同于那些用于接入構造函數的調用,在參數類型數組中使用了字段名:

l         Field getField(String name) -- 獲得命名的公共字段

l         Field[] getFields() -- 獲得類的所有公共字段

l         Field getDeclaredField(String name) -- 獲得類聲明的命名的字段

l         Field[] getDeclaredFields() -- 獲得類聲明的所有字段

用于獲得方法資訊函數:

l         Method getMethod(String name, Class[] params) -- 使用特定的參數類型,獲得命名的公共方法

l         Method[] getMethods() -- 獲得類的所有公共方法

l         Method getDeclaredMethod(String name, Class[] params) -- 使用特寫的參數類型,獲得類聲明的命名的方法

l         Method[] getDeclaredMethods() -- 獲得類聲明的所有方法

1.3開始使用 Reflection:

用于 reflection 的類,如 Method,可以在 java.lang.relfect 包中找到。使用這些類的時候必須要遵循三個步驟:第一步是獲得你想操作的類的 java.lang.Class 對象。在運作中的 Java 程式中,用 java.lang.Class 類來描述類和接口等。

下面就是獲得一個 Class 對象的方法之一:

Class c = Class.forName("java.lang.String");

這條語句得到一個 String 類的類對象。還有另一種方法,如下面的語句:

Class c = int.class;

或者

Class c = Integer.TYPE;

它們可獲得基本類型的類資訊。其中後一種方法中通路的是基本類型的封裝類 (如 Integer) 中預先定義好的 TYPE 字段。

第二步是調用諸如 getDeclaredMethods 的方法,以取得該類中定義的所有方法的清單。

一旦取得這個資訊,就可以進行第三步了——使用 reflection API 來操作這些資訊,如下面這段代碼:

Class c = Class.forName("java.lang.String");

Method m[] = c.getDeclaredMethods();

System.out.println(m[0].toString());

它将以文本方式列印出 String 中定義的第一個方法的原型。

2.處理對象:

如果要作一個開發工具像debugger之類的,你必須能發現filed values,以下是三個步驟:

a.建立一個Class對象

b.通過getField 建立一個Field對象

c.調用Field.getXXX(Object)方法(XXX是Int,Float等,如果是對象就省略;Object是指執行個體).

例如:

import java.lang.reflect.*;

import java.awt.*;

class SampleGet {

   public static void main(String[] args) {

      Rectangle r = new Rectangle(100, 325);

      printHeight(r);

   }

   static void printHeight(Rectangle r) {

      Field heightField;

      Integer heightValue;

      Class c = r.getClass();

      try {

        heightField = c.getField("height");

        heightValue = (Integer) heightField.get(r);

        System.out.println("Height: " + heightValue.toString());

      } catch (NoSuchFieldException e) {

          System.out.println(e);

      } catch (SecurityException e) {

          System.out.println(e);

      } catch (IllegalAccessException e) {

          System.out.println(e);

      }

   }

}

三、安全性和反射:

在處理反射時安全性是一個較複雜的問題。反射經常由架構型代碼使用,由于這一點,我們可能希望架構能夠全面接入代碼,無需考慮正常的接入限制。但是,在其它情況下,不受控制的接入會帶來嚴重的安全性風險,例如當代碼在不值得信任的代碼共享的環境中運作時。

由于這些互相沖突的需求,Java程式設計語言定義一種多級别方法來處理反射的安全性。基本模式是對反射實施與應用于源代碼接入相同的限制:

n         從任意位置到類公共元件的接入

n         類自身外部無任何到私有元件的接入

n         受保護和打包(預設接入)元件的有限接入

不過至少有些時候,圍繞這些限制還有一種簡單的方法。我們可以在我們所寫的類中,擴充一個普通的基本類java.lang.reflect.AccessibleObject 類。這個類定義了一種setAccessible方法,使我們能夠啟動或關閉對這些類中其中一個類的執行個體的接入檢測。唯一的問題在于如果使用了安全性管理器,它将檢測正在關閉接入檢測的代碼是否許可了這樣做。如果未許可,安全性管理器抛出一個例外。

下面是一段程式,在TwoString 類的一個執行個體上使用反射來顯示安全性正在運作:

public class ReflectSecurity {

    public static void main(String[] args) {

        try {

            TwoString ts = new TwoString("a", "b");

            Field field = clas.getDeclaredField("m_s1");

//          field.setAccessible(true);

            System.out.println("Retrieved value is " +

                field.get(inst));

        } catch (Exception ex) {

            ex.printStackTrace(System.out);

        }

    }

}

如果我們編譯這一程式時,不使用任何特定參數直接從指令行運作,它将在field .get(inst)調用中抛出一個IllegalAccessException異常。如果我們不注釋field.setAccessible(true)代碼行,那麼重新編譯并重新運作該代碼,它将編譯成功。最後,如果我們在指令行添加了JVM參數-Djava.security.manager以實作安全性管理器,它仍然将不能通過編譯,除非我們定義了ReflectSecurity類的許可權限。

四、反射性能:

反射是一種強大的工具,但也存在一些不足。一個主要的缺點是對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼并且它滿足我們的要求。這類操作總是慢于隻直接執行相同的操作。

下面的程式是字段接入性能測試的一個例子,包括基本的測試方法。每種方法測試字段接入的一種形式 -- accessSame 與同一對象的成員字段協作,accessOther 使用可直接接入的另一對象的字段,accessReflection 使用可通過反射接入的另一對象的字段。在每種情況下,方法執行相同的計算 -- 循環中簡單的加/乘順序。

程式如下:

public int accessSame(int loops) {

    m_value = 0;

    for (int index = 0; index < loops; index++) {

        m_value = (m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return m_value;

}

public int accessReference(int loops) {

    TimingClass timing = new TimingClass();

    for (int index = 0; index < loops; index++) {

        timing.m_value = (timing.m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return timing.m_value;

}

public int accessReflection(int loops) throws Exception {

    TimingClass timing = new TimingClass();

    try {

        Field field = TimingClass.class.

            getDeclaredField("m_value");

        for (int index = 0; index < loops; index++) {

            int value = (field.getInt(timing) +

                ADDITIVE_VALUE) * MULTIPLIER_VALUE;

            field.setInt(timing, value);

        }

        return timing.m_value;

    } catch (Exception ex) {

        System.out.println("Error using reflection");

        throw ex;

    }

}

在上面的例子中,測試程式重複調用每種方法,使用一個大循環數,進而平均多次調用的時間衡量結果。平均值中不包括每種方法第一次調用的時間,是以初始化時間不是結果中的一個因素。下面的圖清楚的向我們展示了每種方法字段接入的時間:

圖 1:字段接入時間 :

我們可以看出:在前兩副圖中(Sun JVM),使用反射的執行時間超過使用直接接入的1000倍以上。通過比較,IBM JVM可能稍好一些,但反射方法仍舊需要比其它方法長700倍以上的時間。任何JVM上其它兩種方法之間時間方面無任何顯著差異,但IBM JVM幾乎比Sun JVM快一倍。最有可能的是這種差異反映了Sun Hot Spot JVM的專業優化,它在簡單基準方面表現得很糟糕。反射性能是Sun開發1.4 JVM時關注的一個方面,它在反射方法調用結果中顯示。在這類操作的性能方面,Sun 1.4.1 JVM顯示了比1.3.1版本很大的改進。

如果為為建立使用反射的對象編寫了類似的計時測試程式,我們會發現這種情況下的差異不象字段和方法調用情況下那麼顯著。使用newInstance()調用建立一個簡單的java.lang.Object執行個體耗用的時間大約是在Sun 1.3.1 JVM上使用new Object()的12倍,是在IBM 1.4.0 JVM的四倍,隻是Sun 1.4.1 JVM上的兩部。使用Array.newInstance(type, size)建立一個數組耗用的時間是任何測試的JVM上使用new type[size]的兩倍,随着數組大小的增加,差異逐漸縮小。