Java反射:框架設計的靈魂

https://www.jianshu.com/p/1fc45c89e76b

要想理解反射的原理,首先要了解什麼是類型信息。Java讓我們在運行時識別對象和類的信息,主要有兩種方式:一種是傳統的RTTI(Run-Time Type Identification),它假定我們在編譯時已經知道了所有的類型信息;另一種是反射機制,它允許我們在運行時發現和使用類的信息。
使用的前提條件:必須先得到代表的字節碼的Class,Class類用於表示.class文件(字節碼)

一、反射的概述

JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲Java語言的反射機制。
要想解剖一個類,必須先要獲取到該類的字節碼文件對象。而解剖使用的就是Class類中的方法。所以先要獲取到每一個字節碼文件對應的Class類型的對象.。

反射就是把Java類中的各種成分映射成一個個的Java對象。
例如:
一個類有:成員變量、方法、構造方法、包等等信息,利用反射技術可以對一個類進行解剖,把各個組成部分映射成一個個對象。
(其實:一個類中這些成員方法、構造方法,在加入類中都有一個類來描述)
如圖是類的正常加載過程:反射的原理在於class對象。

 

熟悉一下加載的時候:Class對象的由來是將class文件讀入內存,併爲之創建一個Class對象。

類的正常加載過程

二、反射的理解

反射之中包含了一個「反」字,所以想要解釋反射就必須先從「正」開始解釋。

一般情況下,我們使用某個類時必定知道它是什麼類,是用來做什麼的。於是我們直接對這個類進行實例化,之後使用這個類對象進行操作。
如:

 

Phone phone = new Phone(); //直接初始化,「正射」
phone.setPrice(4);

上面這樣子進行類對象的初始化,我們可以理解爲「正」。

而反射則是一開始並不知道我要初始化的類對象是什麼,自然也無法使用 new 關鍵字來創建對象了。

這時候,我們使用 JDK 提供的反射 API 進行反射調用:

 

Class clz = Class.forName("com.xxp.reflect.Phone");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面兩段代碼的執行結果,其實是完全一樣的。但是其思路完全不一樣,第一段代碼在未運行時就已經確定了要運行的類(Phone),而第二段代碼則是在運行時通過字符串值才得知要運行的類(com.xxp.reflect.Phone)。

所以說什麼是反射?反射就是在運行時才知道要操作的類是什麼,並且可以在運行時獲取類的完整構造,並調用對應的方法。

一個簡單的例子:
上面提到的示例程序,其完整的程序代碼如下:

 

public class Phone {
    private int price;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的調用
        Phone phone = new Phone();
        phone.setPrice(5000);
        System.out.println("Phone Price:" + phone.getPrice());
        //使用反射調用
        Class clz = Class.forName("com.xxp.api.Phone");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor phoneConstructor = clz.getConstructor();
        Object phoneObj = phoneConstructor.newInstance();
        setPriceMethod.invoke(phoneObj, 6000);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Phone Price:" + getPriceMethod.invoke(phoneObj));
    }
}

從代碼中可以看到我們使用反射調用了 setPrice 方法,並傳遞了 6000 的值。之後使用反射調用了 getPrice 方法,輸出其價格。上面的代碼整個的輸出結果是:

Phone Price:5000
Phone Price:6000
從這個簡單的例子可以看出,一般情況下我們使用反射獲取一個對象的步驟:

 

//獲取類的 Class 對象實例
Class clz = Class.forName("com.xxp.api.Phone");
//根據 Class 對象實例獲取 Constructor 對象
Constructor phoneConstructor = clz.getConstructor();
//使用 Constructor 對象的 newInstance 方法獲取反射類對象
Object phoneObj = phoneConstructor.newInstance();

而如果要調用某一個方法,則需要經過下面的步驟:

 

//獲取方法的 Method 對象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
//利用 invoke 方法調用方法
setPriceMethod.invoke(phoneObj, 6000);

到這裏,我們已經能夠掌握反射的基本使用。但如果要進一步掌握反射,還需要對反射的常用 API 有更深入的理解。

三、反射的常用API

在 JDK 中,反射相關的 API 可以分爲下面幾個方面:獲取反射的 Class 對象、通過反射創建類對象、通過反射獲取類屬性方法及構造器。

反射常用API:
獲取反射中的Class對象
在反射中,要獲取一個類或調用一個類的方法,我們首先需要獲取到該類的 Class 對象。
在 Java API 中,獲取 Class 類對象有三種方法:

  1. 使用 Class.forName 靜態方法。當知道某類的全路徑名時,可以使用此方法獲取 Class 類對象。用的最多,但可能拋出 ClassNotFoundException 異常。
    Class c1 = Class.forName(“java.lang.String”);

  2. 直接通過 類名.class 的方式得到,該方法最爲安全可靠,程序性能更高。這說明任何一個類都有一個隱含的靜態成員變量 class。這種方法只適合在編譯前就知道操作的 Class。
    Class c2 = String.class;

  3. 通過對象調用 getClass() 方法來獲取,通常應用在:比如你傳過來一個 Object類型的對象,而我不知道你具體是什麼類,用這種方法。

 

String str = new String("Hello");
Class c3 = str.getClass();

需要注意的是:一個類在 JVM 中只會有一個 Class 實例,即我們對上面獲取的 c1、c2和c3進行 equals 比較,發現都是true。

通過反射創建類對象
通過反射創建類對象主要有兩種方式:通過 Class 對象的 newInstance() 方法、通過 Constructor 對象的 newInstance() 方法。

  1. 通過 Class 對象的 newInstance() 方法。
    Class clz = Phone.class;
    Phone phone = (Phone)clz.newInstance();

  2. 通過 Constructor 對象的 newInstance() 方法

 

Class clz = Phone.class;
Constructor constructor = clz.getConstructor();
Phone phone= (Phone)constructor.newInstance();

通過 Constructor 對象創建類對象可以選擇特定構造方法,而通過 Class 對象則只能使用默認的無參數構造方法。下面的代碼就調用了一個有參數的構造方法進行了類對象的初始化。

 

Class clz = Phone.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Phone phone = (Phone)constructor.newInstance("華爲",6666);

通過反射獲取類屬性、方法、構造器
我們通過 Class 對象的 getFields() 方法可以獲取 Class 類的屬性,但無法獲取私有屬性。

 

Class clz = Phone.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

輸出結果是:
price
而如果使用 Class 對象的 getDeclaredFields() 方法則可以獲取包括私有屬性在內的所有屬性:

 

Class clz = Phone.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

輸出結果是:
name
price
與獲取類屬性一樣,當我們去獲取類方法、類構造器時,如果要獲取私有方法或私有構造器,則必須使用有 declared 關鍵字的方法。

附:查閱 API 可以看到 Class 有很多方法:
  getName():獲得類的完整名字。
  getFields():獲得類的public類型的屬性。
  getDeclaredFields():獲得類的所有屬性。包括private 聲明的和繼承類
  getMethods():獲得類的public類型的方法。
  getDeclaredMethods():獲得類的所有方法。包括private 聲明的和繼承類
  getMethod(String name, Class[] parameterTypes):獲得類的特定方法,name參數指定方法的名字,parameterTypes 參數指定方法的參數類型。
  getConstructors():獲得類的public類型的構造方法。
  getConstructor(Class[] parameterTypes):獲得類的特定構造方法,parameterTypes 參數指定構造方法的參數類型。
  newInstance():通過類的不帶參數的構造方法創建這個類的一個對象。

四、反射源碼解析

當我們懂得了如何使用反射後,今天我們就來看看 JDK 源碼中是如何實現反射的。或許大家平時沒有使用過反射,但是在開發 Web 項目的時候會遇到過下面的異常:

 

java.lang.NullPointerException 
...
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:369)

可以看到異常堆棧指出了異常在 Method 的第 369 的 invoke 方法中,其實這裏指的 invoke 方法就是我們反射調用方法中的 invoke。

 

Method method = clz.getMethod("setPrice", int.class); 
method.invoke(object, 6);   //就是這裏的invoke方法

例如我們經常使用的 Spring 配置中,經常會有相關 Bean 的配置:

 

<bean class="com.xxp.Phone">
</bean>

當我們在 XML 文件中配置了上面這段配置之後,Spring 便會在啓動的時候利用反射去加載對應的 Phone類。而當 Apple 類不存在或發生啓發異常時,異常堆棧便會將異常指向調用的 invoke 方法。

從這裏可以看出,我們平常很多框架都使用了反射,而反射中最重要的就是 Method 類的 invoke 方法了。

五、反射總結

我們知道反射機制允許程序在運行時取得任何一個已知名稱的class的內部信息,包括包括其modifiers(修飾符),fields(屬性),methods(方法)等,並可於運行時改變fields內容或調用methods。那麼我們便可以更靈活的編寫代碼,代碼可以在運行時裝配,無需在組件之間進行源代碼鏈接,降低代碼的耦合度;還有動態代理的實現;JDBC原生代碼註冊驅動;hibernate 的實體類;Spring的AOP等等。但是凡事都有兩面性,反射使用不當會造成很高的資源消耗!

六、new對象和反射得到對象的區別

  1. 在使用反射的時候,必須確保這個類已經加載並已經連接了。使用new的時候,這個類可以沒有被加載,也可以已經被加載。
  2. new關鍵字可以調用任何public構造方法,而反射只能調用無參構造方法。
  3. new關鍵字是強類型的,效率相對較高。 反射是弱類型的,效率低。
  4. 反射提供了一種更加靈活的方式創建對象,得到對象的信息。如Spring 中AOP等的使用,動態代理的使用,都是基於反射的。解耦。



作者:MChopin
鏈接:https://www.jianshu.com/p/1fc45c89e76b
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章