「深入Java」類(lèi)型信息:RTTI和反射

歡迎轉(zhuǎn)載膜楷,但請(qǐng)保留作者鏈接:http://www.reibang.com/p/5a6bad3752d9

有Java中滋恬,我們?nèi)绾卧谶\(yùn)行時(shí)識(shí)別類(lèi)和對(duì)象的信息?有兩種方法挑围,一是傳統(tǒng)的RTTI,另一種是反射糖荒。

1.RTTI Run-Time Type Infomation 運(yùn)行時(shí)類(lèi)型信息

為什么需要RTTI杉辙?

越是優(yōu)秀的面向?qū)ο笤O(shè)計(jì),越是強(qiáng)調(diào)高內(nèi)聚低耦合捶朵,正如依賴倒轉(zhuǎn)原則所說(shuō):“無(wú)論是高層模塊還是低層模塊蜘矢,都應(yīng)該針對(duì)抽象編程”狂男。

比如說(shuō)我們有一個(gè)抽象父類(lèi):

Shape draw()

以下是三個(gè)具體類(lèi):

Circle draw()
Square draw()
Triangle draw()

某些情況下,我們持有Shape品腹,但卻遠(yuǎn)遠(yuǎn)不夠——因?yàn)槲覀兿胍槍?duì)它的具體類(lèi)型進(jìn)行特殊處理岖食,然而我們的設(shè)計(jì)完全針對(duì)抽象,所以在當(dāng)前上下文環(huán)境中無(wú)法判斷具體類(lèi)型舞吭。
因?yàn)镽TTI的存在泡垃,使得我們?cè)诓黄茐脑O(shè)計(jì)的前提下得以達(dá)到目的。


Class類(lèi)與Class對(duì)象

事實(shí)上羡鸥,每一個(gè)類(lèi)都持有其對(duì)應(yīng)的Class類(lèi)的對(duì)象的引用(Object類(lèi)中的getClass()能讓我們獲取到它)蔑穴,其中包含著與類(lèi)相關(guān)的信息。
非常容易注意到惧浴,針對(duì)每一個(gè)類(lèi)存和,編譯Java文件會(huì)生成一個(gè)二進(jìn)制.class文件,這其中就保存著該類(lèi)對(duì)應(yīng)的Class對(duì)象的信息衷旅。

.class是用于供類(lèi)加載器使用的文件

Java程序在運(yùn)行之前并沒(méi)有被完全加載捐腿,各個(gè)部分是在需要時(shí)才被加載的。

為了使用類(lèi)而作的準(zhǔn)備包含三步:

  1. 加載柿顶。由類(lèi)加載器執(zhí)行叙量,查找字節(jié)碼,創(chuàng)建一個(gè)Class對(duì)象九串。
  2. 鏈接绞佩。驗(yàn)證字節(jié)碼,為靜態(tài)域分配存儲(chǔ)空間猪钮,如果必需的話品山,會(huì)解析這個(gè)類(lèi)創(chuàng)建的對(duì)其他類(lèi)的所有引用(比如說(shuō)該類(lèi)持有static域)。
  3. 初始化烤低。如果該類(lèi)有超類(lèi)肘交,則對(duì)其初始化,執(zhí)行靜態(tài)初始化器[注]和靜態(tài)初始化塊扑馁。

注:原文為static initializers涯呻,經(jīng)查看Thinking in Java,其意應(yīng)為靜態(tài)域在定義處的初始化腻要,如:
static Dog d = new Dog(0);复罐。

所有的類(lèi)都是在對(duì)其第一次使用時(shí),動(dòng)態(tài)加載到JVM中去的雄家。當(dāng)程序創(chuàng)建第一個(gè)對(duì)類(lèi)的靜態(tài)成員的引用時(shí)效诅,JVM會(huì)使用類(lèi)加載器來(lái)根據(jù)類(lèi)名查找同名的.class——一旦某個(gè)類(lèi)的Class對(duì)象被載入內(nèi)存,它就被用來(lái)創(chuàng)建這個(gè)類(lèi)的所有對(duì)象。構(gòu)造器也是類(lèi)的靜態(tài)方法乱投,使用new操作符創(chuàng)建新對(duì)象會(huì)被當(dāng)作對(duì)類(lèi)的靜態(tài)成員的引用咽笼。
注意特例:如果一個(gè)static final值是編譯期常量,讀取這個(gè)值不需要對(duì)類(lèi)進(jìn)行初始化戚炫。所以說(shuō)對(duì)于不變常量剑刑,我們總是應(yīng)該使用static final修飾。


Class.forName(String str)

Class類(lèi)有一個(gè)很有用的靜態(tài)方法forName(String str)双肤,可以讓我們對(duì)于某個(gè)類(lèi)不進(jìn)行創(chuàng)建就得到它的Class對(duì)象的引用施掏,例如這個(gè)樣子:

try {
     Class toyClass = Class.forName("com.duanze.Toy"); // 注意必須使用全限定名
} catch (ClassNotFoundException e) {

}

然而,使用forName(String str)有一個(gè)副作用:如果Toy類(lèi)沒(méi)有被加載杨伙,調(diào)用它會(huì)觸發(fā)Toy類(lèi)的static子句(靜態(tài)初始化塊)其监。

與之相比,更好用的是類(lèi)字面常量限匣,像是這樣:

Class toyClass = Toy.class;

支持編譯時(shí)檢查抖苦,所以不會(huì)拋出異常。使用類(lèi)字面常量創(chuàng)建Class對(duì)象的引用與forName(String str)不同米死,不會(huì)觸發(fā)Toy類(lèi)的static子句(靜態(tài)初始化塊)锌历。所以,更簡(jiǎn)單更安全更高效峦筒。
類(lèi)字面常量支持類(lèi)究西、接口、數(shù)組物喷、基本數(shù)據(jù)類(lèi)型卤材。


×拓展×

Class.forName(String className)使用裝載當(dāng)前類(lèi)的類(lèi)裝載器來(lái)裝載指定類(lèi)。因?yàn)?code>class.forName(String className)方法內(nèi)部調(diào)用了Class.forName(className, true, this.getClass().getClassLoader())方法峦失,如你所見(jiàn)扇丛,第三個(gè)參數(shù)就是指定類(lèi)裝載器,顯而易見(jiàn)尉辑,它指定的是裝載當(dāng)前類(lèi)的類(lèi)裝載器的實(shí)例帆精,也就是this.getClass().getClassLoader();

你可以選擇手動(dòng)指定裝載器:

ClassLoader cl = new  ClassLoader();   
Class c1 = cl.loadClass(String className, boolean resolve );  

更詳細(xì)的參考


范化的Class引用

通過(guò)范型以及通配符,我們能對(duì)Class對(duì)象的引用進(jìn)行類(lèi)型限定隧魄,像是:

Class<Integer> intClass = int.class; // 注意右邊是基本數(shù)據(jù)類(lèi)型的類(lèi)字面常量

這樣做的好處是能讓編譯器進(jìn)行額外的類(lèi)型檢查卓练。
知道了這一點(diǎn)以后,我們可以把之前的例子改寫(xiě)一下:

Class toyClass = Toy.class;
Class<?> toyClass = Toy.class;

雖然這兩句是等價(jià)的购啄,但從可讀性來(lái)說(shuō)Class<?>要優(yōu)于Class襟企,這說(shuō)明編程者并不是由于疏忽而選擇了非具體版本,而是特意選擇了非具體版本闸溃。


Class.newInstance()

既然拿到了包含著類(lèi)信息的Class對(duì)象的引用整吆,我們理應(yīng)可以構(gòu)造出一個(gè)類(lèi)的實(shí)例拱撵。Class.newInstance()就是這樣一個(gè)方法辉川,比如:

// One
try {
    Class<?> toyClass = Class.forName("com.duanze.Toy"); 
    Object obj = toyClass.newInstance();
} catch (ClassNotFoundException e) {

}

// Two
Class<?> toyClass = Toy.class;
Object obj = toyClass.newInstance();

使用newInstance()創(chuàng)建的類(lèi)表蝙,必須帶有默認(rèn)構(gòu)造器。
由于toyClass僅僅只是一個(gè)Class對(duì)象引用乓旗,在編譯期不具備更進(jìn)一步的類(lèi)型信息府蛇,所以你使用newInstance()時(shí)只會(huì)得到一個(gè)Object引用。如果你需要拿到確切類(lèi)型屿愚,需要這樣做:

Class<Toy> toyClass = Toy.class;
Toy obj = toyClass.newInstance();

但是汇跨,如果你遇到下面的情況,還是只能拿到Object引用:

Class<SubToy> subToyClass = SubToy.class;
Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父類(lèi)Toy的Class對(duì)象引用
// This won't compile:
// Class<Toy> upClass = subToy.getSuperclass();
// Only produces Object:
Object obj = upClass.newInstance();

雖然從常理上來(lái)講妆距,編譯器應(yīng)該在編譯期就能知道SubToy的超類(lèi)是Toy穷遂,但實(shí)際上卻并不支持這樣寫(xiě):

// This won't compile:
Class<Toy> upClass = subToy.getSuperclass();

而只能夠接受:

Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父類(lèi)Toy

這看上去有些奇怪,但現(xiàn)狀就是如此娱据,我們惟有接受蚪黑。好在這并不是什么大問(wèn)題,因?yàn)?strong>轉(zhuǎn)型操作并不困難中剩。


類(lèi)型檢查

在進(jìn)行類(lèi)型轉(zhuǎn)換之前忌穿,可以使用instanceof關(guān)鍵字進(jìn)行類(lèi)型檢查,像是:

if ( x instanceof Shape ) {
     Shape s = (Shape)x;
}

一般情況下instanceof已經(jīng)夠用结啼,但有些時(shí)候你可能需要更動(dòng)態(tài)的測(cè)試途徑:Class.isInstance(Class clz):

Class<Shape> s = Shape.class;
s.isInstance(x);

可以看到掠剑,與instanceof相比,isInstance()的左右兩邊都是可變的郊愧,這一動(dòng)態(tài)性有時(shí)可以讓大量包裹在if else...中的instanceof縮減為一句朴译。


2.反射

不知道你注意到了沒(méi)有,以上使用的RTTI都具有一個(gè)共同的限制:在編譯時(shí)属铁,編譯器必須知道所有要通過(guò)RTTI來(lái)處理的類(lèi)眠寿。

但有的時(shí)候,你獲取了一個(gè)對(duì)象引用红选,然而其對(duì)應(yīng)的類(lèi)并不在你的程序空間中澜公,怎么辦?(這種情況并不少見(jiàn)喇肋,比如說(shuō)你從磁盤(pán)文件或者網(wǎng)絡(luò)中獲取了一串字串坟乾,并且被告知這一串字串代表了一個(gè)類(lèi),這個(gè)類(lèi)在編譯器為你的程序生成代碼之后才會(huì)出現(xiàn)蝶防。)

Class類(lèi)和java.lang.reflect類(lèi)庫(kù)一同對(duì)反射的概念提供了支持甚侣。反射機(jī)制并沒(méi)有什么神奇之處,當(dāng)通過(guò)反射與一個(gè)未知類(lèi)型的對(duì)象打交道時(shí)间学,JVM只是簡(jiǎn)單地檢查這個(gè)對(duì)象殷费,看它屬于哪個(gè)特定的類(lèi)印荔。因此,那個(gè)類(lèi)的.class對(duì)于JVM來(lái)說(shuō)必須是可獲取的详羡,要么在本地機(jī)器上仍律,要么從網(wǎng)絡(luò)獲取。所以對(duì)于RTTI和反射之間的真正區(qū)別只在于:

  • RTTI实柠,編譯器在編譯時(shí)打開(kāi)和檢查.class文件
  • 反射水泉,運(yùn)行時(shí)打開(kāi)和檢查.class文件

明白了以上概念后,什么getFields(),getMethods(),getConstructors()之類(lèi)的方法基本上全都可以望文生義了窒盐。

我們可以看一下Android開(kāi)發(fā)中經(jīng)常用的對(duì)于ActionBar草则,讓Overflow中的選項(xiàng)顯示圖標(biāo)這一效果是怎么做出來(lái)的:

/*
overflow中的Action按鈕應(yīng)不應(yīng)該顯示圖標(biāo),
是由MenuBuilder這個(gè)類(lèi)的setOptionalIconsVisible方法來(lái)決定的蟹漓,
如果我們?cè)趏verflow被展開(kāi)的時(shí)候給這個(gè)方法傳入true炕横,
那么里面的每一個(gè)Action按鈕對(duì)應(yīng)的圖標(biāo)就都會(huì)顯示出來(lái)了。
*/

@Override
public boolean onMenuOpened(int featureId, Menu menu) {
    if (featureId == Window.FEATURE_ACTION_BAR && menu != null) {
        if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
            try {
                // Boolean.TYPE 同 boolean.class
                Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); 
                // 通過(guò)setAccessible(true)葡粒,確狈莸睿可以調(diào)用方法——即使是private方法
                m.setAccessible(true);
                // 相當(dāng)于:menu.setOptionalIconsVisible(true) 
                m.invoke(menu, true);
            } catch (Exception e) {
            }
        }
    }
    return super.onMenuOpened(featureId, menu);
}

×拓展:動(dòng)態(tài)代理×

Java中對(duì)于反射的一處重要使用為動(dòng)態(tài)代理,可以參考這篇IBM developerworks的文章


參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末塔鳍,一起剝皮案震驚了整個(gè)濱河市伯铣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轮纫,老刑警劉巖腔寡,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異掌唾,居然都是意外死亡放前,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)糯彬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)凭语,“玉大人,你說(shuō)我怎么就攤上這事撩扒∷迫樱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵搓谆,是天一觀的道長(zhǎng)炒辉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)泉手,這世上最難降的妖魔是什么黔寇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮斩萌,結(jié)果婚禮上缝裤,老公的妹妹穿的比我還像新娘屏轰。我一直安慰自己,他們只是感情好憋飞,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布霎苗。 她就那樣靜靜地躺著,像睡著了一般搀崭。 火紅的嫁衣襯著肌膚如雪叨粘。 梳的紋絲不亂的頭發(fā)上猾编,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天瘤睹,我揣著相機(jī)與錄音,去河邊找鬼答倡。 笑死轰传,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘪撇。 我是一名探鬼主播获茬,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼倔既!你這毒婦竟也來(lái)了恕曲?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤渤涌,失蹤者是張志新(化名)和其女友劉穎佩谣,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體实蓬,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茸俭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了安皱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片调鬓。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖酌伊,靈堂內(nèi)的尸體忽然破棺而出腾窝,到底是詐尸還是另有隱情,我是刑警寧澤居砖,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布虹脯,位于F島的核電站,受9級(jí)特大地震影響悯蝉,放射性物質(zhì)發(fā)生泄漏归形。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一鼻由、第九天 我趴在偏房一處隱蔽的房頂上張望暇榴。 院中可真熱鬧厚棵,春花似錦、人聲如沸蔼紧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)奸例。三九已至彬犯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間查吊,已是汗流浹背谐区。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逻卖,地道東北人宋列。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像评也,于是被迫代替她去往敵國(guó)和親炼杖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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