JAVA RTTI
基礎類可接收我們發給派生類的任何消息,因為兩者擁有完全一致的接口。我們要做的全部事情就是從派生上溯造型,而且永遠不需要回過頭來檢查對象的準確類型是什么。所有細節都已通過多態性獲得了完美的控制。
但經過細致的研究,我們發現擴展接口對于一些特定問題來說是特別有效的方案。可將其稱為“類似于”關系,因為擴展后的派生類“類似于”基礎類——它們有相同的基礎接口——但它增加了一些特性,要求用額外的方法加以實現。如下所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ? ? ? ? ? ??
盡管這是一種有用和明智的做法,但它也有一個缺點:派生類中對接口擴展的那一部分不可在基礎類中使用。所以一旦上溯造型,就不可再調用新方法:
若在此時不進行上溯造型,則不會出現此類問題。但在許多情況下,都需要重新核實對象的準確類型,使自己能訪問那個類型的擴展方法。
下溯造型與運行期類型標識:
由于我們在上溯造型期間丟失了具體的類型信息,所以為了獲取具體的類型信息——亦即在分級結構中向下移動——我們必須使用 “下溯造型”技術。然而,我們知道一個上溯造型肯定是安全的;基礎類不可能再擁有一個比派生類更大的接口。因此,我們通過基礎類接口發送的每一條消息都肯定能夠接收到。但在進行下溯造型的時候,我們并不真的知道是什么具體類型。
? ? ? ? ? ? ? ? ? ? ? ? ? ??
為解決這個問題,必須有一種辦法能夠保證下溯造型正確進行。只有這樣,我們才不會冒然造型成一種錯誤的類型。
在某些語言中(如C++),為了進行保證“類型安全”的下溯造型,必須采取特殊的操作。但在Java中,所有造型都會自動得到檢查和核實!所以即使我們只是進行一次普通的括弧造型,進入運行期以后,仍然會毫無留情地對這個造型進行檢查,保證它的確是我們希望的那種類型。如果不是,就會得到一個ClassCastException(類造型違例)。在運行期間對類型進行檢查的行為叫作“運行期類型標識”(RTTI)。
當然RTTI的意義遠不僅僅反映在造型處理上。例如,在試圖下溯造型之前,可通過一種方法了解自己處理的是什么類型。
Java在運行期間查找對象和類信息。這主要采取兩種形式:一種是“傳統”RTTI,它假定我們已在編譯和運行期擁有所有類型;另一種是Java1.1特有的“反射”機制,利用它可在運行期獨立查找類信息。
RTTI:
下面的例子,它利用了多態性。常規類型是Shape類,派生類為:Circle,Square和Triangle。
這是一個典型的類結構示意圖,基礎類位于頂部,派生類向下延展。面向對象編程的基本目標是用大量代碼控制基礎類型的句柄,draw()在所有派生類里都會被覆蓋。而且由于它是一個動態綁定方法,所以即使通過一個基類句柄調用它,也有表現出正確的行為。這正是多態性的作用。
因為Vector只容納了對象,由于Java中的所有東西(除基本數據類型外)都是對象,所以Vector也能容納Shape對象。但在上溯造型至Object的過程中,任何特殊的信息都會丟失,其中甚至包括對象是幾何形狀這一事實。對Vector來說,它們只是Object。用nextElement()將一個元素從Vector提取出來的時候,情況變得稍微有些復雜。由于Vector只容納Object,所以nextElement()會自然地產生一個Object句柄。但我們知道它實際是個Shape句柄,而且希望將Shape消息發給那個對象。所以需要用傳統的"(Shape)"方式造型成一個Shape。這是RTTI最基本的形式,因為在Java中,所有造型都會在運行期間得到檢查,以確保其正確性。那正是RTTI的意義所在:在運行期,對象的類型會得到鑒定。
在目前這種情況下,RTTI造型只實現了一部分:Object造型成Shape,而不是造型成Circle,Square或者Triangle。那是由于我們目前能夠肯定的唯一事實就是Vector里充斥著幾何形狀,而不知它們的具體類別。在編譯期間,我們肯定的依據是我們自己的規則;而在編譯期間,卻是通過造型來肯定這一點,即編譯期間的強制類型轉換。
現在的局面會由多態性控制,而且會為Shape調用適當的方法,以便判斷句柄到底是提供Circle,Square,還是提供給Triangle。而且在一般情況下,必須保證采用多態性方案。因為我們希望自己的代碼盡可能少知道一些與對象的具體類型有關的情況,只將注意力放在某一類對象(這里是Shape)的常規信息上。只有這樣,我們的代碼才更易實現、理解以及修改。所以說多態是面向對象程序設計的一個常規目標。
然而,若碰到一個特殊的程序設計問題,只有在知道常規句柄的確切類型后,才能最容易地解決這個問題,這個時候又該怎么辦呢?舉個例子來說,我們有時候想讓自己的用戶將某一具體類型的幾何形狀(如三角形)全都變成紫色,以便突出顯示它們,并快速找出這一類型的所有形狀。此時便要用到RTTI技術,用它查詢某個Shape句柄引用的準確類型是什么。
Class對象:
為理解RTTI在Java里如何工作,首先必須了解類型信息在運行期是如何表示的。這時要用到一個名為“Class對象”的特殊形式的對象,其中包含了與類有關的信息。
對于作為程序一部分的每個類,它們都有一個Class對象。換言之,每次寫一個新類時,同時也會創建一個Class對象(更恰當地說,是保存在一個完全同名的.class文件中)。在運行期,一旦我們想生成那個類的一個對象,用于執行程序的Java虛擬機(JVM)首先就會檢查那個類型的Class對象是否已經載入。若尚未載入,JVM就會查找同名的.class文件,并將其載入。所以Java程序啟動時并不是完全載入的,這一點與許多傳統語言都不同。一旦那個類型的Class對象進入內存,就用它創建那一類型的所有對象。
迄今為止,我們已知的RTTI形式包括:
(1) 經典造型,如"(Shape)",它用RTTI確保object造型Shape的正確性,并在遇到一個失敗的造型后產生一個ClassCastException違例。
(2) 代表對象類型的Class對象。可查詢Class對象,獲取有用的運行期資料。
在C++中,經典的"(Shape)"造型并不執行RTTI,但Java是利用了RTTI。C++它只是簡單地告訴編譯器將對象當作新類型處理。而Java要在造型前執行類型檢查,這通常叫作“類型安全”的下溯造型。盡管我們明確知道Circle也是一個Shape,所以編譯器能夠自動上溯造型,但卻不能保證一個Shape肯定是一個Circle。因此,編譯器不允許自動下溯造型,除非明確指定一次這樣的造型。
RTTI在Java中存在三種形式。
1、關鍵字instanceof告訴我們對象是不是一個特定類型的實例(Instance即“實例”)。它會返回一個布爾值,以便以問題的形式使用,就象下面這樣:
使用方式:實例 instanceof 類
if(x instanceof Dog)
((Dog)x).bark();
將x造型至一個Dog前,上面的if語句會檢查對象x是否從屬于Dog類。進行造型前,如果沒有其他信息可以告訴自己對象的類型,那么instanceof的使用是非常重要的——否則會得到一個ClassCastException違例。對instanceof有一個比較小的限制:只可將其與一個已命名的類型比較,不能同Class對象作對比。
2、isInstance()方法,動態的instanceof?
使用方式:類.isInstance(實例)
RTTI語法:
package rtti; import static net.mindview.util.Print.*;interface HasBatteries { }interface Waterproof { }interface Shoots { }class Toy {Toy() {}Toy(int i) {} }class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {FancyToy() {super(1);} }public class ToyTest {static void printInfo(Class cc) { //自變量為class句柄print("\n====>");print("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");print("Simple name: " + cc.getSimpleName());print("Full name : " + cc.getCanonicalName());}public static void main(String[] args) {Class c = null;try {c = Class.forName("rtti.FancyToy");} catch (ClassNotFoundException e) {print("Can't find FancyToy");System.exit(1);}printInfo(c);for (Class face : c.getInterfaces()) //獲取“FctoryToy”類接口并迭代printInfo(face);Class up = c.getSuperclass();Object obj = null;try {// Requires default constructor:obj = up.newInstance(); //使用class句柄創建實例} catch (InstantiationException e) {print("Cannot instantiate");System.exit(1);} catch (IllegalAccessException e) {print("Cannot access");System.exit(1);}printInfo(obj.getClass());} } Class.getInterfaces方法會返回Class對象的一個數組,用于表示包含在Class對象內的接口。
若有一個Class對象,也可以用getSuperclass()查詢該對象的直接基礎類是什么。當然,這種做會返回一個Class句柄,可用它作進一步的查詢。這意味著在運行期的時候,完全有機會調查到對象的完整層次結構。
若從表面看,Class的newInstance()方法似乎是克隆(clone())一個對象的另一種手段。但兩者是有區別的。利用newInstance(),我們可在沒有現成對象供“克隆”的情況下新建一個對象。就象上面的程序演示的那樣,當時沒有Toy對象,只有up——即up的Class對象的一個句柄。利用它可以實現“虛擬構建器”。換言之,我們表達:“盡管我不知道你的準確類型是什么,但請你無論如何都正確地創建自己。”在上述例子中,up只是一個Class句柄,編譯期間并不知道進一步的類型信息。一旦新建了一個實例后,可以得到Object句柄。但那個句柄指向一個Toy對象。當然,如果要將除Object能夠接收的其他任何消息發出去,首先必須進行一些調查研究,再進行造型。除此以外,用newInstance()創建的類必須有一個默認構建器。沒有辦法用newInstance()創建擁有非默認構建器的對象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一節討論)卻允許我們動態地使用類里的任何構建器。
程序中的最后一個方法是printInfo(),它取得一個Class句柄,通過getName()獲得它的名字,并用interface()調查它是不是一個接口。
反射:運行期類信息
如果不知道一個對象的準確類型,RTTI會幫助我們調查。但卻有一個限制:類型必須是在編譯期間已知的,否則就不能用RTTI調查它,進而無法展開下一步的工作。換言之,編譯器必須明確知道RTTI要處理的所有類。
從表面看,這似乎并不是一個很大的限制,但假若得到的是一個不在自己程序空間內的對象的句柄,這時又會怎樣呢?例如,假設我們從磁盤或者網絡獲得一系列字節,而且被告知那些字節代表一個類。由于編譯器在編譯代碼時并不知道那個類的情況,所以怎樣才能順利地使用這個類呢?
在傳統的程序設計環境中,出現這種情況的概率或許很小。但當我們轉移到一個規模更大的編程世界中,卻必須對這個問題加以高度重視。第一個要注意的是基于組件的程序設計。在這種環境下,我們用“快速應用開發”(RAD)模型來構建程序項目。RAD一般是在應用程序構建工具中內建的。
在運行期查詢類信息的另一個原動力是通過網絡創建與執行位于遠程系統上的對象。這就叫作“遠程方法調用”(RMI),它允許Java程序(版本1.1以上)使用由多臺機器發布或分布的對象。這種對象的分布可能是由多方面的原因引起的:可能要做一件計算密集型的工作,想對它進行分割,讓處于空閑狀態的其他機器分擔部分工作,從而加快處理進度。某些情況下,可能需要將用于控制特定類型任務(比如多層客戶/服務器架構中的“運作規則”)的代碼放置在一臺特殊的機器上,使這臺機器成為對那些行動進行描述的一個通用儲藏所。而且可以方便地修改這個場所,使其對系統內的所有方面產生影響(這是一種特別有用的設計思路,因為機器是獨立存在的,所以能輕易修改軟件!)。
在Java 1.1中,Class類(本章前面已有詳細論述)得到了擴展,可以支持“反射”的概念。針對Field,Method以及Constructor類(每個都實現了Memberinterface——成員接口),它們都新增了一個庫:java.lang.reflect。這些類型的對象都是JVM在運行期創建的,用于代表未知類里對應的成員。這樣便可用構建器創建新對象,用get()和set()方法讀取和修改與Field對象關聯的字段,以及用invoke()方法調用與Method對象關聯的方法。此外,我們可調用方法getFields(),getMethods(),getConstructors(),分別返回用于表示字段、方法以及構建器的對象數組(在聯機文檔中,還可找到與Class類有關的更多的資料)。因此,匿名對象的類信息可在運行期被完整的揭露出來,而在編譯期間不需要知道任何東西。
大家要認識的很重要的一點是“反射”并沒有什么神奇的地方。通過“反射”同一個未知類型的對象打交道時,JVM只是簡單地檢查那個對象,并調查它從屬于哪個特定的類(就象以前的RTTI那樣)。但在這之后,在我們做其他任何事情之前,Class對象必須載入。因此,用于那種特定類型的.class文件必須能由JVM調用(要么在本地機器內,要么可以通過網絡取得)。所以RTTI和“反射”之間唯一的區別就是對RTTI來說,編譯器會在編譯期打開和檢查.class文件。換句話說,我們可以用“普通”方式調用一個對象的所有方法;但對“反射”來說,.class文件在編譯期間是不可使用的,而是由運行期環境打開和檢查。
一個類方法提取器
很少需要直接使用反射工具;之所以在語言中提供它們,僅僅是為了支持其他Java特性,比如對象序列化、Java Beans以及RMI。但是,我們許多時候仍然需要動態提取與一個類有關的資料。其中特別有用的工具便是一個類方法提取器。正如前面指出的那樣,若檢視類定義源碼或者聯機文檔,只能看到在那個類定義中被定義或覆蓋的方法,基礎類那里還有大量資料拿不到。幸運的是,“反射”做到了這一點,可用它寫一個簡單的工具,令其自動展示整個接口。下面便是具體的程序:
Class方法getMethods()和getConstructors()可以分別返回Method和Constructor的一個數組。每個類都提供了進一步的方法,可解析出它們所代表的方法的名字、參數以及返回值。但也可以象這樣一樣只使用toString(),生成一個含有完整方法簽名的字串。代碼剩余的部分只是用于提取命令行信息,判斷特定的簽名是否與我們的目標字串相符(使用indexOf()),并打印出結果。
這里便用到了“反射”技術,因為由Class.forName()產生的結果不能在編譯期間獲知,所以所有方法簽名信息都會在運行期間提取。若研究一下聯機文檔中關于“反”(Reflection)的那部分文字,就會發現它已提供了足夠多的支持,可對一個編譯期完全未知的對象進行實際的設置以及發出方法調用。同樣地,這也屬于幾乎完全不用我們操心的一個步驟——Java自己會利用這種支持,所以程序設計環境能夠控制Java Beans——但它無論如何都是非常有趣的。
一個有趣的試驗是運行java ShowMehods ShowMethods。這樣做可得到一個列表,其中包括一個public默認構建器,盡管我們在代碼中看見并沒有定義一個構建器。我們看到的是由編譯器自動合成的那一個構建器。如果隨之將ShowMethods設為一個非public類(即換成“友好”類),合成的默認構建器便不會在輸出結果中出現。合成的默認構建器會自動獲得與類一樣的訪問權限。
ShowMethods的輸出仍然有些“不爽”。例如,下面是通過調用java ShowMethods java.lang.String得到的輸出結果的一部分:
若能去掉象java.lang這樣的限定詞,結果顯然會更令人滿意。有鑒于此,可引入上一章介紹的StreamTokenizer類,解決這個問題:
ShowMethodsClean方法非常接近前一個ShowMethods,只是它取得了Method和Constructor數組,并將它們轉換成單個String數組。隨后,每個這樣的String對象都在StripQualifiers.Strip()里“過”一遍,刪除所有方法限定詞。正如大家看到的那樣,此時用到了StreamTokenizer和String來完成這個工作。
假如記不得一個類是否有一個特定的方法,而且不想在聯機文檔里逐步檢查類結構,或者不知道那個類是否能對某個對象(如Color對象)做某件事情,該工具便可節省大量編程時間。
第17章提供了這個程序的一個GUI版本,可在自己寫代碼的時候運行它,以便快速查找需要的東西。
利用RTTI可根據一個匿名的基礎類句柄調查出類型信息。但正是由于這個原因,新手們極易誤用它,因為有些時候多態性方法便足夠了。對那些以前習慣程序化編程的人來說,極易將他們的程序組織成一系列switch語句。他們可能用RTTI做到這一點,從而在代碼開發和維護中損失多態性技術的重要價值。Java的要求是讓我們盡可能地采用多態性,只有在極特別的情況下才使用RTTI。
但為了利用多態性,要求我們擁有對基礎類定義的控制權,因為有些時候在程序范圍之內,可能發現基礎類并未包括我們想要的方法。若基礎類來自一個庫,或者由別的什么東西控制著,RTTI便是一種很好的解決方案:可繼承一個新類型,然后添加自己的額外方法。在代碼的其他地方,可以偵測自己的特定類型,并調用那個特殊的方法。這樣做不會破壞多態性以及程序的擴展能力,因為新類型的添加不要求查找程序中的switch語句。但在需要新特性的主體中添加新代碼時,就必須用RTTI偵測自己特定的類型。
從某個特定類的利益的角度出發,在基礎類里加入一個特性后,可能意味著從那個基礎類衍生的其他所有類都必須獲得一些無意義的“雞肋”。這使得接口變得含義模糊。若有人從那個基礎類繼承,且必須覆蓋抽象方法,這一現象便會使他們陷入困擾。比如現在用一個類結構來表示樂器(Instrument)。假定我們想清潔管弦樂隊中所有適當樂器的通氣音栓(Spit Valve),此時的一個辦法是在基礎類Instrument中置入一個ClearSpitValve()方法。但這樣做會造成一個誤區,因為它暗示著打擊樂器和電子樂器中也有音栓。針對這種情況,RTTI提供了一個更合理的解決方案,可將方法置入特定的類中(此時是Wind,即“通氣口”)——這樣做是可行的。但事實上一種更合理的方案是將prepareInstrument()置入基礎類中。初學者剛開始時往往看不到這一點,一般會認定自己必須使用RTTI。
最后,RTTI有時能解決效率問題。若代碼大量運用了多態性,但其中的一個對象在執行效率上很有問題,便可用RTTI找出那個類型,然后寫一段適當的代碼,改進其效率。
總結
“多態性”意味著“不同的形式”。在面向對象的程序設計中,我們有相同的外觀(基礎類的通用接口)以及使用那個外觀的不同形式:動態綁定或組織的、不同版本的方法。
通過這一章的學習,大家已知道假如不利用數據抽象以及繼承技術,就不可能理解、甚至去創建多態性的一個例子。多態性是一種不可獨立應用的特性(就象一個switch語句),只可與其他元素協同使用。我們應將其作為類總體關系的一部分來看待。人們經常混淆Java其他的、非面向對象的特性,比如方法過載等,這些特性有時也具有面向對象的某些特征。但不要被愚弄:如果以后沒有綁定,就不成其為多態性。
為使用多態性乃至面向對象的技術,特別是在自己的程序中,必須將自己的編程視野擴展到不僅包括單獨一個類的成員和消息,也要包括類與類之間的一致性以及它們的關系。盡管這要求學習時付出更多的精力,但卻是非常值得的,因為只有這樣才可真正有效地加快自己的編程速度、更好地組織代碼、更容易做出包容面廣的程序以及更易對自己的代碼進行維護與擴展。
總結
- 上一篇: Exchange 服务器查看版本号
- 下一篇: C++学习手记五:C++流操作