# 11.3 反射:運行期類信息
如果不知道一個對象的準確類型,RTTI會幫助我們調查。但卻有一個限制:類型必須是在編譯期間已知的,否則就不能用RTTI調查它,進而無法展開下一步的工作。換言之,編譯器必須明確知道RTTI要處理的所有類。
從表面看,這似乎并不是一個很大的限制,但假若得到的是一個不在自己程序空間內的對象的引用,這時又會怎樣呢?事實上,對象的類即使在編譯期間也不可由我們的程序使用。例如,假設我們從磁盤或者網絡獲得一系列字節,而且被告知那些字節代表一個類。由于編譯器在編譯代碼時并不知道那個類的情況,所以怎樣才能順利地使用這個類呢?
在傳統的程序設計環境中,出現這種情況的概率或許很小。但當我們轉移到一個規模更大的編程世界中,卻必須對這個問題加以高度重視。第一個要注意的是基于組件的程序設計。在這種環境下,我們用“快速應用開發”(RAD)模型來構建程序項目。RAD一般是在應用程序構建工具中內建的。這是編制程序的一種可視途徑(在屏幕上以窗體的形式出現)。可將代表不同組件的圖標拖曳到窗體中。隨后,通過設定這些組件的屬性或者值,進行正確的配置。設計期間的配置要求任何組件都是可以“例示”的(即可以自由獲得它們的實例)。這些組件也要揭示出自己的一部分內容,允許程序員讀取和設置各種值。此外,用于控制GUI事件的組件必須揭示出與相應的方法有關的信息,以便RAD環境幫助程序員用自己的代碼覆蓋這些由事件驅動的方法。“反射”提供了一種特殊的機制,可以偵測可用的方法,并產生方法名。通過Java Beans(第13章將詳細介紹),Java 1.1為這種基于組件的程序設計提供了一個基礎結構。
在運行期查詢類信息的另一個原動力是通過網絡創建與執行位于遠程系統上的對象。這就叫作“遠程方法調用”(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`文件在編譯期間是不可使用的,而是由運行期環境打開和檢查。
## 11.3.1 一個類方法提取器
很少需要直接使用反射工具;之所以在語言中提供它們,僅僅是為了支持其他Java特性,比如對象序列化(第10章介紹)、Java Beans以及RMI(本章后面介紹)。但是,我們許多時候仍然需要動態提取與一個類有關的資料。其中特別有用的工具便是一個類方法提取器。正如前面指出的那樣,若檢視類定義源碼或者聯機文檔,只能看到在那個類定義中被定義或覆蓋的方法,基類那里還有大量資料拿不到。幸運的是,“反射”做到了這一點,可用它寫一個簡單的工具,令其自動展示整個接口。下面便是具體的程序:
```
//: ShowMethods.java
// Using Java 1.1 reflection to show all the
// methods of a class, even if the methods are
// defined in the base class.
import java.lang.reflect.*;
public class ShowMethods {
static final String usage =
"usage: \n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or: \n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
if(args.length == 1) {
for (int i = 0; i < m.length; i++)
System.out.println(m[i].toString());
for (int i = 0; i < ctor.length; i++)
System.out.println(ctor[i].toString());
}
else {
for (int i = 0; i < m.length; i++)
if(m[i].toString()
.indexOf(args[1])!= -1)
System.out.println(m[i].toString());
for (int i = 0; i < ctor.length; i++)
if(ctor[i].toString()
.indexOf(args[1])!= -1)
System.out.println(ctor[i].toString());
}
} catch (ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
} ///:~
```
`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`得到的輸出結果的一部分:
```
public boolean
java.lang.String.startsWith(java.lang.String,int)
public boolean
java.lang.String.startsWith(java.lang.String)
public boolean
java.lang.String.endsWith(java.lang.String)
```
若能去掉象`java.lang`這樣的限定詞,結果顯然會更令人滿意。有鑒于此,可引入上一章介紹的`StreamTokenizer`類,解決這個問題:
```
//: ShowMethodsClean.java
// ShowMethods with the qualifiers stripped
// to make the results easier to read
import java.lang.reflect.*;
import java.io.*;
public class ShowMethodsClean {
static final String usage =
"usage: \n" +
"ShowMethodsClean qualified.class.name\n" +
"To show all methods in class or: \n" +
"ShowMethodsClean qualif.class.name word\n" +
"To search for methods involving 'word'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
// Convert to an array of cleaned Strings:
String[] n =
new String[m.length + ctor.length];
for(int i = 0; i < m.length; i++) {
String s = m[i].toString();
n[i] = StripQualifiers.strip(s);
}
for(int i = 0; i < ctor.length; i++) {
String s = ctor[i].toString();
n[i + m.length] =
StripQualifiers.strip(s);
}
if(args.length == 1)
for (int i = 0; i < n.length; i++)
System.out.println(n[i]);
else
for (int i = 0; i < n.length; i++)
if(n[i].indexOf(args[1])!= -1)
System.out.println(n[i]);
} catch (ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
}
class StripQualifiers {
private StreamTokenizer st;
public StripQualifiers(String qualified) {
st = new StreamTokenizer(
new StringReader(qualified));
st.ordinaryChar(' '); // Keep the spaces
}
public String getNext() {
String s = null;
try {
if(st.nextToken() !=
StreamTokenizer.TT_EOF) {
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = null;
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = new String(st.sval);
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
}
} catch(IOException e) {
System.out.println(e);
}
return s;
}
public static String strip(String qualified) {
StripQualifiers sq =
new StripQualifiers(qualified);
String s = "", si;
while((si = sq.getNext()) != null) {
int lastDot = si.lastIndexOf('.');
if(lastDot != -1)
si = si.substring(lastDot + 1);
s += si;
}
return s;
}
} ///:~
```
`ShowMethodsClean`方法非常接近前一個`ShowMethods`,只是它取得了`Method`和`Constructor`數組,并將它們轉換成單個`String`數組。隨后,每個這樣的`String`對象都在`StripQualifiers.Strip()`里“過”一遍,刪除所有方法限定詞。正如大家看到的那樣,此時用到了`StreamTokenizer`和`String`來完成這個工作。
假如記不得一個類是否有一個特定的方法,而且不想在聯機文檔里逐步檢查類結構,或者不知道那個類是否能對某個對象(如`Color`對象)做某件事情,該工具便可節省大量編程時間。
第17章提供了這個程序的一個GUI版本,可在自己寫代碼的時候運行它,以便快速查找需要的東西。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物