大白話說Java反射:入門廊移、使用糕簿、原理

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

一般情況下懂诗,我們使用某個(gè)類時(shí)必定知道它是什么類蜂嗽,是用來(lái)做什么的。于是我們直接對(duì)這個(gè)類進(jìn)行實(shí)例化殃恒,之后使用這個(gè)類對(duì)象進(jìn)行操作植旧。

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

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

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

這時(shí)候完沪,我們使用 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é)果,其實(shí)是完全一樣的。但是其思路完全不一樣覆积,第一段代碼在未運(yùn)行時(shí)就已經(jīng)確定了要運(yùn)行的類(Apple)听皿,而第二段代碼則是在運(yùn)行時(shí)通過字符串值才得知要運(yùn)行的類(com.chenshuyi.reflect.Apple)。

所以說什么是反射宽档?

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

一個(gè)簡(jiǎn)單的例子

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

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à)格。上面的代碼整個(gè)的輸出結(jié)果是:

Apple Price:5
Apple Price:14

從這個(gè)簡(jiǎn)單的例子可以看出降传,一般情況下我們使用反射獲取一個(gè)對(duì)象的步驟:

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

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

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

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

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

反射常用API

獲取反射中的Class對(duì)象

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

在 Java API 中姐赡,獲取 Class 類對(duì)象有三種方法:

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

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

第二種,使用 .class 方法。

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

Class clz = String.class;

第三種摩幔,使用類對(duì)象的 getClass() 方法川陆。

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

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

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

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

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

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

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

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

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

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

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

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

輸出結(jié)果是:

price

而如果使用 Class 對(duì)象的 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)造器時(shí)手素,如果要獲取私有方法或私有構(gòu)造器鸳址,則必須使用有 declared 關(guān)鍵字的方法。

反射源碼解析

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

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

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

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

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

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

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

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

下面我們來(lái)看看 JDK 的 invoke 方法到底做了些什么。

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

image

那么 MethodAccessor 又是什么呢沛贪?

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

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

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

image

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

image

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

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

image

所以說ReflectionFactory 類的 newMethodAccessor 方法最終返回 DelegatingMethodAccessorImpl 類對(duì)象沪铭。所以我們?cè)谇懊娴?ma.invoke() 里壮池,其將會(huì)進(jìn)入 DelegatingMethodAccessorImpl 類的 invoke 方法中。

image

進(jìn)入 DelegatingMethodAccessorImpl 類的 invoke 方法后杀怠,這里調(diào)用了 delegate 屬性的 invoke 方法椰憋,它又有兩個(gè)實(shí)現(xiàn)類,分別是:DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl赔退。按照我們前面說到的橙依,這里的 delegate 其實(shí)是一個(gè) NativeMethodAccessorImpl 對(duì)象,所以這里會(huì)進(jìn)入 NativeMethodAccessorImpl 的 invoke 方法硕旗。

image

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

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

"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 機(jī)制互订。初次加載字節(jié)碼實(shí)現(xiàn)反射,使用 Method.invoke() 和 Constructor.newInstance() 加載花費(fèi)的時(shí)間是使用原生代碼加載花費(fèi)時(shí)間的 3 - 4 倍痘拆。這使得那些頻繁使用反射的應(yīng)用需要花費(fèi)更長(zhǎng)的啟動(dòng)時(shí)間仰禽。

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.

為了避免這種痛苦的加載時(shí)間,我們?cè)诘谝淮渭虞d的時(shí)候重用了 JVM 的入口纺蛆,之后切換到字節(jié)碼實(shí)現(xiàn)的實(shí)現(xiàn)吐葵。

就像注釋里說的,實(shí)際的 MethodAccessor 實(shí)現(xiàn)有兩個(gè)版本桥氏,一個(gè)是 Native 版本温峭,一個(gè)是 Java 版本。

Native 版本一開始啟動(dòng)快字支,但是隨著運(yùn)行時(shí)間邊長(zhǎng)凤藏,速度變慢奸忽。Java 版本一開始加載慢,但是隨著運(yùn)行時(shí)間邊長(zhǎng)揖庄,速度變快栗菜。正是因?yàn)閮煞N存在這些問題,所以第一次加載的時(shí)候我們會(huì)發(fā)現(xiàn)使用的是 NativeMethodAccessorImpl 的實(shí)現(xiàn)蹄梢,而當(dāng)反射調(diào)用次數(shù)超過 15 次之后疙筹,則使用 MethodAccessorGenerator 生成的 MethodAccessorImpl 對(duì)象去實(shí)現(xiàn)反射。

Method 類的 invoke 方法整個(gè)流程可以表示成如下的時(shí)序圖:

image

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

原文可見:大白話說Java反射:入門、使用玛痊、原理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汰瘫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子擂煞,更是在濱河造成了極大的恐慌混弥,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件对省,死亡現(xiàn)場(chǎng)離奇詭異蝗拿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蒿涎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門哀托,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人劳秋,你說我怎么就攤上這事仓手。” “怎么了玻淑?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵嗽冒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我岁忘,道長(zhǎng)辛慰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任干像,我火速辦了婚禮帅腌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘麻汰。我一直安慰自己速客,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布五鲫。 她就那樣靜靜地躺著溺职,像睡著了一般。 火紅的嫁衣襯著肌膚如雪位喂。 梳的紋絲不亂的頭發(fā)上浪耘,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音塑崖,去河邊找鬼七冲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛规婆,可吹牛的內(nèi)容都是我干的澜躺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼抒蚜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼掘鄙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嗡髓,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤操漠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后饿这,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浊伙,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年蛹稍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吧黄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡唆姐,死狀恐怖拗慨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奉芦,我是刑警寧澤赵抢,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站声功,受9級(jí)特大地震影響烦却,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜先巴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一其爵、第九天 我趴在偏房一處隱蔽的房頂上張望冒冬。 院中可真熱鬧,春花似錦摩渺、人聲如沸简烤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)横侦。三九已至,卻和暖如春绰姻,著一層夾襖步出監(jiān)牢的瞬間枉侧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工狂芋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榨馁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓银酗,卻偏偏與公主長(zhǎng)得像辆影,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子黍特,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容