欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java反射入門、原理與使用方法詳解

 更新時間:2020年04月23日 14:31:24   作者:陳樹義  
這篇文章主要介紹了Java反射入門、原理與使用方法,結(jié)合實例形式詳細(xì)分析了java反射的概念、原理、使用方法與相關(guān)操作注意事項,需要的朋友可以參考下

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

一般情況下,我們使用某個類時必定知道它是什么類,是用來做什么的。于是我們直接對這個類進(jìn)行實例化,之后使用這個類對象進(jìn)行操作。

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

上面這樣子進(jìn)行類對象的初始化,我們可以理解為「正」。

而反射則是一開始并不知道我要初始化的類對象是什么,自然也無法使用 new 關(guān)鍵字來創(chuàng)建對象了。

這時候,我們使用 JDK 提供的反射 API 進(jìn)行反射調(diào)用:

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

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

所以說什么是反射?

反射就是在運行時才知道要操作的類是什么,并且可以在運行時獲取類的完整構(gòu)造,并調(diào)用對應(yīng)的方法。

一個簡單的例子

上面提到的示例程序,其完整的程序代碼如下:

public class Apple {

  private int price;

  public int getPrice() {
    return price;
  }

  public void setPrice(int price) {
    this.price = price;
  }

  public static void main(String[] args) throws Exception{
    //正常的調(diào)用
    Apple apple = new Apple();
    apple.setPrice(5);
    System.out.println("Apple Price:" + apple.getPrice());
    //使用反射調(diào)用
    Class clz = Class.forName("com.chenshuyi.api.Apple");
    Method setPriceMethod = clz.getMethod("setPrice", int.class);
    Constructor appleConstructor = clz.getConstructor();
    Object appleObj = appleConstructor.newInstance();
    setPriceMethod.invoke(appleObj, 14);
    Method getPriceMethod = clz.getMethod("getPrice");
    System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
  }
}

從代碼中可以看到我們使用反射調(diào)用了 setPrice 方法,并傳遞了 14 的值。之后使用反射調(diào)用了 getPrice 方法,輸出其價格。上面的代碼整個的輸出結(jié)果是:

Apple Price:5
Apple Price:14

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

  • 獲取類的 Class 對象實例
Class clz = Class.forName("com.zhenai.api.Apple");
  • 根據(jù) Class 對象實例獲取 Constructor 對象
Constructor appleConstructor = clz.getConstructor();
  • 使用 Constructor 對象的 newInstance 方法獲取反射類對象
Object appleObj = appleConstructor.newInstance();

而如果要調(diào)用某一個方法,則需要經(jīng)過下面的步驟:

  • 獲取方法的 Method 對象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
  • 利用 invoke 方法調(diào)用方法
setPriceMethod.invoke(appleObj, 14);

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

在 JDK 中,反射相關(guān)的 API 可以分為下面幾個方面:獲取反射的 Class 對象、通過反射創(chuàng)建類對象、通過反射獲取類屬性方法及構(gòu)造器。

反射常用API

獲取反射中的Class對象

在反射中,要獲取一個類或調(diào)用一個類的方法,我們首先需要獲取到該類的 Class 對象。

在 Java API 中,獲取 Class 類對象有三種方法:

第一種,使用 Class.forName 靜態(tài)方法。當(dāng)你知道該類的全路徑名時,你可以使用該方法獲取 Class 類對象。

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

第二種,使用 .class 方法。

這種方法只適合在編譯前就知道操作的 Class。

Class clz = String.class;

第三種,使用類對象的 getClass() 方法。

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

通過反射創(chuàng)建類對象

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

第一種:通過 Class 對象的 newInstance() 方法。

Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();

第二種:通過 Constructor 對象的 newInstance() 方法

Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();

通過 Constructor 對象創(chuàng)建類對象可以選擇特定構(gòu)造方法,而通過 Class 對象則只能使用默認(rèn)的無參數(shù)構(gòu)造方法。下面的代碼就調(diào)用了一個有參數(shù)的構(gòu)造方法進(jìn)行了類對象的初始化。

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("紅富士", 15);

通過反射獲取類屬性、方法、構(gòu)造器

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

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

輸出結(jié)果是:

price

而如果使用 Class 對象的 getDeclaredFields() 方法則可以獲取包括私有屬性在內(nèi)的所有屬性:

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

輸出結(jié)果是:

name
price

與獲取類屬性一樣,當(dāng)我們?nèi)カ@取類方法、類構(gòu)造器時,如果要獲取私有方法或私有構(gòu)造器,則必須使用有 declared 關(guān)鍵字的方法。

反射源碼解析

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

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

可以看到異常堆棧指出了異常在 Method 的第 497 的 invoke 方法中,其實這里指的 invoke 方法就是我們反射調(diào)用方法中的 invoke。

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

例如我們經(jīng)常使用的 Spring 配置中,經(jīng)常會有相關(guān) Bean 的配置:

<bean class="com.chenshuyi.Apple">
</bean>

當(dāng)我們在 XML 文件中配置了上面這段配置之后,Spring 便會在啟動的時候利用反射去加載對應(yīng)的 Apple 類。而當(dāng) Apple 類不存在或發(fā)生啟發(fā)異常時,異常堆棧便會將異常指向調(diào)用的 invoke 方法。

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

下面我們來看看 JDK 的 invoke 方法到底做了些什么。

進(jìn)入 Method 的 invoke 方法我們可以看到,一開始是進(jìn)行了一些權(quán)限的檢查,最后是調(diào)用了 MethodAccessor 類的 invoke 方法進(jìn)行進(jìn)一步處理,如下圖紅色方框所示。

那么 MethodAccessor 又是什么呢?

其實 MethodAccessor 是一個接口,定義了方法調(diào)用的具體操作,而它有三個具體的實現(xiàn)類:

  • sun.reflect.DelegatingMethodAccessorImpl
  • sun.reflect.MethodAccessorImpl
  • sun.reflect.NativeMethodAccessorImpl

而要看 ma.invoke() 到底調(diào)用的是哪個類的 invoke 方法,則需要看看 MethodAccessor 對象返回的到底是哪個類對象,所以我們需要進(jìn)入 acquireMethodAccessor() 方法中看看。

從 acquireMethodAccessor() 方法我們可以看到,代碼先判斷是否存在對應(yīng)的 MethodAccessor 對象,如果存在那么就復(fù)用之前的 MethodAccessor 對象,否則調(diào)用 ReflectionFactory 對象的 newMethodAccessor 方法生成一個 MethodAccessor 對象。

在 ReflectionFactory 類的 newMethodAccessor 方法里,我們可以看到首先是生成了一個 NativeMethodAccessorImpl 對象,再這個對象作為參數(shù)調(diào)用 DelegatingMethodAccessorImpl 類的構(gòu)造方法。

這里的實現(xiàn)是使用了代理模式,將 NativeMethodAccessorImpl 對象交給 DelegatingMethodAccessorImpl 對象代理。我們查看 DelegatingMethodAccessorImpl 類的構(gòu)造方法可以知道,其實是將 NativeMethodAccessorImpl 對象賦值給 DelegatingMethodAccessorImpl 類的 delegate 屬性。

所以說ReflectionFactory 類的 newMethodAccessor 方法最終返回 DelegatingMethodAccessorImpl 類對象。所以我們在前面的 ma.invoke() 里,其將會進(jìn)入 DelegatingMethodAccessorImpl 類的 invoke 方法中。

進(jìn)入 DelegatingMethodAccessorImpl 類的 invoke 方法后,這里調(diào)用了 delegate 屬性的 invoke 方法,它又有兩個實現(xiàn)類,分別是:DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl。按照我們前面說到的,這里的 delegate 其實是一個 NativeMethodAccessorImpl 對象,所以這里會進(jìn)入 NativeMethodAccessorImpl 的 invoke 方法。

而在 NativeMethodAccessorImpl 的 invoke 方法里,其會判斷調(diào)用次數(shù)是否超過閥值(numInvocations)。如果超過該閥值,那么就會生成另一個MethodAccessor 對象,并將原來 DelegatingMethodAccessorImpl 對象中的 delegate 屬性指向最新的 MethodAccessor 對象。

到這里,其實我們可以知道 MethodAccessor 對象其實就是具體去生成反射類的入口。通過查看源碼上的注釋,我們可以了解到 MethodAccessor 對象的一些設(shè)計信息。

"Inflation" mechanism. Loading bytecodes to implement Method.invoke() and Constructor.newInstance() currently costs 3-4x more than an invocation via native code for the first invocation (though subsequent invocations have been benchmarked to be over 20x faster).Unfortunately this cost increases startup time for certain applications that use reflection intensively (but only once per class) to bootstrap themselves.

Inflation 機制。初次加載字節(jié)碼實現(xiàn)反射,使用 Method.invoke() 和 Constructor.newInstance() 加載花費的時間是使用原生代碼加載花費時間的 3 - 4 倍。這使得那些頻繁使用反射的應(yīng)用需要花費更長的啟動時間。

To avoid this penalty we reuse the existing JVM entry points for the first few invocations of Methods and Constructors and then switch to the bytecode-based implementations. Package-private to be accessible to NativeMethodAccessorImpl and NativeConstructorAccessorImpl.

為了避免這種痛苦的加載時間,我們在第一次加載的時候重用了 JVM 的入口,之后切換到字節(jié)碼實現(xiàn)的實現(xiàn)。

就像注釋里說的,實際的 MethodAccessor 實現(xiàn)有兩個版本,一個是 Native 版本,一個是 Java 版本。

Native 版本一開始啟動快,但是隨著運行時間邊長,速度變慢。Java 版本一開始加載慢,但是隨著運行時間邊長,速度變快。正是因為兩種存在這些問題,所以第一次加載的時候我們會發(fā)現(xiàn)使用的是 NativeMethodAccessorImpl 的實現(xiàn),而當(dāng)反射調(diào)用次數(shù)超過 15 次之后,則使用 MethodAccessorGenerator 生成的 MethodAccessorImpl 對象去實現(xiàn)反射。

Method 類的 invoke 方法整個流程可以表示成如下的時序圖:

講到這里,我們了解了 Method 類的 invoke 方法的具體實現(xiàn)方式。知道了原來 invoke 方法內(nèi)部有兩種實現(xiàn)方式,一種是 native 原生的實現(xiàn)方式,一種是 Java 實現(xiàn)方式,這兩種各有千秋。而為了最大化性能優(yōu)勢,JDK 源碼使用了代理的設(shè)計模式去實現(xiàn)最大化性能。

相關(guān)文章

  • 解決springboot與springcloud版本兼容問題(附版本兼容表)

    解決springboot與springcloud版本兼容問題(附版本兼容表)

    在基于spring boot搭建spring cloud時,創(chuàng)建eureka后啟動服務(wù)發(fā)生報錯,本文給大家介紹了解決springboot與springcloud版本兼容問題的幾種方案,需要的朋友可以參考下
    2024-02-02
  • Java編程guava RateLimiter實例解析

    Java編程guava RateLimiter實例解析

    這篇文章主要介紹了Java編程guava RateLimiter實例解析,具有一定借鑒價值,需要的朋友可以參考下
    2018-01-01
  • MyBatis-Plus 批量保存的操作方法

    MyBatis-Plus 批量保存的操作方法

    在項目開發(fā)中,需要插入批量插入20多萬條數(shù)據(jù),通過日志觀察,發(fā)現(xiàn)在調(diào)用MyBatis-Plus中的saveBatch()方法性能非常的差,本篇文章主要分享一下saveBatch()的原理以及使用的注意事項,感興趣的朋友跟隨小編一起看看吧
    2024-01-01
  • Spring避免循環(huán)依賴的策略詳解

    Spring避免循環(huán)依賴的策略詳解

    在Spring框架中,循環(huán)依賴是指兩個或多個bean相互依賴對方,形成一個閉環(huán),這在應(yīng)用啟動時可能導(dǎo)致BeanCurrentlyInCreationException異常,本文給大家介紹了Spring中如何避免循環(huán)依賴,需要的朋友可以參考下
    2024-02-02
  • java利用Calendar類打印日歷

    java利用Calendar類打印日歷

    這篇文章主要為大家詳細(xì)介紹了java利用Calendar類打印日歷,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • java selenium處理Iframe中的元素示例

    java selenium處理Iframe中的元素示例

    本文主要介紹java selenium處理Iframe中的元素,這里整理了相關(guān)資料并附有示例代碼和實現(xiàn)方法,有需要的小伙伴可以參考下
    2016-08-08
  • 解決springboot無法注入JpaRepository的問題

    解決springboot無法注入JpaRepository的問題

    這篇文章主要介紹了解決springboot無法注入JpaRepository的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • springboot restTemplate連接池整合方式

    springboot restTemplate連接池整合方式

    這篇文章主要介紹了springboot restTemplate連接池整合方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • Spring Cloud zuul自定義統(tǒng)一異常處理實現(xiàn)方法

    Spring Cloud zuul自定義統(tǒng)一異常處理實現(xiàn)方法

    這篇文章主要介紹了Spring Cloud zuul自定義統(tǒng)一異常處理實現(xiàn),需要的朋友可以參考下
    2018-02-02
  • Java實現(xiàn)的對稱加密算法3DES定義與用法示例

    Java實現(xiàn)的對稱加密算法3DES定義與用法示例

    這篇文章主要介紹了Java實現(xiàn)的對稱加密算法3DES定義與用法,結(jié)合實例形式簡單分析了Java 3DES加密算法的相關(guān)定義與使用技巧,需要的朋友可以參考下
    2018-04-04

最新評論