10:檢測(cè)類型
運(yùn)行時(shí)類型識(shí)別(run-time type identification,縮寫為RTTI)。
為什么會(huì)需要RTTI
collection是一種工具,它只有一種用途,就是要為你保管其它對(duì)象。因此出于通用性的考慮,這些collection應(yīng)該能持有任何東西。所以它們持有Object。
Class對(duì)象
想要知道JAVA的RTTI是如何工作的,你就必須首先知道程序運(yùn)行的時(shí)候,類型信息是怎樣表示的。這是由一種特殊的,保存類的信息的,叫做“Class對(duì)象(Class object)”的對(duì)象來完成。實(shí)際上類的“常規(guī)”對(duì)象是由Class對(duì)象創(chuàng)建的。
程序里的每個(gè)類都要有一個(gè)Class對(duì)象。也就是說,每次你撰寫并且編譯了一個(gè)新的類的時(shí)候,你就創(chuàng)建了一個(gè)新的Class對(duì)象(而且可以這么說,這個(gè)對(duì)象會(huì)存儲(chǔ)在同名的.class文件里)。程序運(yùn)行時(shí),當(dāng)你需要?jiǎng)?chuàng)建一個(gè)那種類的對(duì)象的時(shí)候,JVM會(huì)檢查它是否裝載了那個(gè)Class對(duì)象。如果沒有, JVM就會(huì)去找那個(gè).class文件,然后裝載。由此也可知道,Java程序在啟動(dòng)的時(shí)候并沒有完全裝載,這點(diǎn)同許多傳統(tǒng)語言是不一樣的。
Class.forName("一個(gè)類的名字");
這是一個(gè)Class的static方法(所有的Class對(duì)象所共有的)。Class對(duì)象同其它對(duì)象一樣,也可以用reference來操控(這是裝載器要干的),而要想獲取其reference, forName()就是一個(gè)辦法。它要一個(gè)表示這個(gè)類的名字的String作參數(shù)(一定要注意拼寫喝大小寫!)。這個(gè)方法會(huì)返回Class的 reference,還有一個(gè)副作用,看看這個(gè)String所說的那個(gè)類裝載了沒有,要是還沒有那就馬上裝載。如果Class.forName()沒有找到它要裝載的類,就會(huì)拋出一個(gè)ClassNotFoundException。
Class常數(shù)
Java還提供了一種獲取Class對(duì)象的reference的方法:“class常數(shù)(class literal)”。
類的名字.class;
這種寫法不但更簡單,而且也更安全,因?yàn)樗窃诰幾g時(shí)做檢查的。此外由于沒有方法調(diào)用,它的執(zhí)行效率也更高一些。
Class常數(shù)不但能用于普通類,也可以用于接口,數(shù)組和primitive類型。此外,每種primitive的wrapper類還有一個(gè)標(biāo)準(zhǔn)的,名為 TYPE的數(shù)據(jù)成員。這個(gè)TYPE能返回“與這種primitive相關(guān)聯(lián)的wrapper類”的Class對(duì)象的reference,就像這樣:
... 等同于 ...
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE
我喜歡盡量使用“.class”,因?yàn)檫@種寫法與普通類的保持一致。
轉(zhuǎn)換之前先作檢查
到目前為止,你看到的RTTI的形式有:
1。經(jīng)典的類型轉(zhuǎn)換:如“(Shape)”,這種轉(zhuǎn)換要經(jīng)過RTTI的檢查。要是做了錯(cuò)誤的轉(zhuǎn)換,它就會(huì)拋出ClassCastException。
2.代表對(duì)象類型的Class對(duì)象。你可以在運(yùn)行的時(shí)候查詢Class對(duì)象,以此來獲取所需的信息。
如果不進(jìn)行明確的類型轉(zhuǎn)換的話,編譯器時(shí)不會(huì)讓你把對(duì)象賦給派生類的reference的。
Java里面還有第三種RTTI的形式。這就是instanceof關(guān)鍵詞,它會(huì)告訴你對(duì)象是不是某個(gè)類的實(shí)例。它返回的是一個(gè)boolean值。
使用類常數(shù)
動(dòng)態(tài)的instanceof
isInstance()能完全替代instanceof。
instanceof vs. Class的相等性
RTTI的語法
Class.getInterfaces()方法會(huì)返回一個(gè)Class對(duì)象的數(shù)組。數(shù)組中的對(duì)象分別表示它所實(shí)現(xiàn)的接口。
如果你手上有一個(gè)Class對(duì)象,你還能用getSuperclass()問出它最近的那個(gè)父類。當(dāng)然,這會(huì)返回一個(gè)Class的reference,于是你可以接著問,程序運(yùn)行的時(shí)候,你能以此發(fā)現(xiàn)對(duì)象的完整的關(guān)系。
Class的newInstance()方法就像是另一種clone()對(duì)象的方法。但是,你卻可以用newInstance()憑空創(chuàng)建出一個(gè)新的對(duì)象。
printInfo()方法,它拿一個(gè)Class對(duì)象的reference作參數(shù),用getName()提取類的名字,用isInterface()判斷它是不是接口。這樣,你就能僅憑Class對(duì)象就找出所有你想知道的這個(gè)對(duì)象的信息了。
Reflection:運(yùn)行時(shí)的類信息
Java以JavaBeans的形式提供了基于組件的編程的支持。
通過網(wǎng)絡(luò)在遠(yuǎn)程機(jī)器上創(chuàng)建對(duì)象并運(yùn)行程序。這被成為“遠(yuǎn)程方法調(diào)用(Remote Method Invocation縮寫是RMI)”。它能讓一個(gè)Java程序?qū)?duì)象分布到很多機(jī)器上。
除了Class類,還有一個(gè)類庫,java.lang.reflect也支持reflection。這個(gè)類庫里面有Field,Method,和 Constructor類(它們都實(shí)現(xiàn)了Member接口)運(yùn)行時(shí),JVM會(huì)創(chuàng)建一些這種類的對(duì)象來代表未知類的各個(gè)成員。然后,你就能用 Constructor來創(chuàng)建新的對(duì)象,用get()和set()來讀取和修改與Field隊(duì)形愛女嘎相關(guān)聯(lián)的成員數(shù)據(jù),用invoke()方法調(diào)用與 Method對(duì)象相關(guān)聯(lián)的方法了。此外,你還能用getFields(),getMethods(),getConstructors()之類的方法,獲取表示成員數(shù)據(jù),方法或構(gòu)造函數(shù)的對(duì)象數(shù)組。由此,即便編譯時(shí)什么信息都得不到,你也有辦法能在運(yùn)行時(shí)問出匿名對(duì)象的全部類型信息了。
有一點(diǎn)很重要,reflection不是什么妖術(shù)。當(dāng)你用reflection與未知類的對(duì)象打交道的時(shí)候,JVM(會(huì)和普通的RTTI一樣)先看看這個(gè)對(duì)象是屬于那個(gè)具體類型的,但是此后,它還是得先裝載Class對(duì)象才能工作。也就是,不管是從本地還是從網(wǎng)絡(luò),反正JVM必須拿到那個(gè).class文件。所以RTTI同reflection的真正區(qū)別在于,RTTI是在編譯時(shí)讓編譯器打開并且檢查.class文件。換句話說,你是在通過“正常”途徑調(diào)用對(duì)象的方法。而對(duì)reflection來說,編譯時(shí)是得不到.class文件的;所以它是在運(yùn)行時(shí)打開并檢查那個(gè)文件。
一個(gè)提取類的方法的程序
一般來說,你不太會(huì)直接使用reflection;Java之所以要有這種功能是要用它來支持一些憋的特性,比如對(duì)象的序列化和JavaBeans。不過在有些情況下,能動(dòng)態(tài)提取類的信息還是很有用的。
Class的getMethods()和getConstructors()方法分別會(huì)返回一個(gè)Method和一個(gè)Constructor數(shù)組。這兩個(gè)類又包括一些“能把它們所代表的方法的名字,參數(shù),返回值全部拆解開來”的方法。不過你也可以像這里所作的,只用toString()去獲取一個(gè)包括這個(gè)方法的全部特征簽名的String。剩下的代碼就是用來抽取命令行信息,以及判斷方法特征是否與你輸入的字符串相匹配(用indexOf()),并且把匹配的方法列出來的。
總結(jié):
RTTI能讓你用一個(gè)匿名的基類reference來獲取對(duì)象的確切類型的信息。在不懂多臺(tái)方法調(diào)用的時(shí)候,這么作是理所當(dāng)然的,因此新手們會(huì)自然而然的想到它,于是就用錯(cuò)了地方,對(duì)很多從面向過程的編程語言轉(zhuǎn)過來的人來說,剛開始的時(shí)候,它們還不習(xí)慣扔掉switch語句。于是當(dāng)他們用RTTI來編程的時(shí)候,就會(huì)錯(cuò)過多態(tài)性所帶來的編程和代碼維護(hù)方面的好處。Java的本義是讓你在程序里面全程使用多態(tài)性,知識(shí)在萬不得已的情況下才使用RTTI。
但是要想正確地使用多臺(tái)方法調(diào)用,你就必須要能控制基類的定義,因?yàn)楫?dāng)你擴(kuò)展程序的時(shí)候,可能會(huì)發(fā)現(xiàn)基類里面沒有你想要的方法。如果這個(gè)基類是來自類庫的,或是由別人控制的,那么RTTI就成解決方案了:你可以繼承一個(gè)新的類,然后加上你自己的方法。在程序的其他地方,你可以檢測(cè)出這個(gè)類型,調(diào)用那些特殊的方法。這樣做不會(huì)破壞多態(tài)性,也不影響程序的擴(kuò)展性,因?yàn)榧右粋€(gè)新的類型不會(huì)要你去到處修改switch語句。但是,如果是在程序的主體部分加入要使用新特性的嗲馬的話,你就必須使用RTTI來檢查對(duì)象的確切類型了。
RTTI還會(huì)被用來解決效率問題。假設(shè)你寫了一個(gè)很好的多臺(tái)程序,但是運(yùn)行的時(shí)候發(fā)現(xiàn),有個(gè)對(duì)象反映奇慢。于是,你就可以用RTTI把則個(gè)對(duì)象撿出來,然后專門針對(duì)它的問題寫代碼以提高程序的運(yùn)行效率,不過編程的時(shí)候切忌去過早有話代碼。這是一個(gè)很有誘惑的陷阱。最好還是先讓程序跑起來,然后再判斷一下它跑得是不是夠快了。只有覺得它還不夠快,你才應(yīng)該去著手解決效率問題--用profiler。
2005年03月19日 12:55 PM