# 11.1 對RTTI的需要
請考慮下面這個熟悉的類結構例子,它利用了多態性。常規類型是`Shape`類,而特別派生出來的類型是`Circle`,`Square`和`Triangle`。

這是一個典型的類結構示意圖,基類位于頂部,派生類向下延展。面向對象編程的基本目標是用大量代碼控制基類型(這里是`Shape`)的引用,所以假如決定添加一個新類(比如`Rhomboid`,從`Shape`派生),從而對程序進行擴展,那么不會影響到原來的代碼。在這個例子中,`Shape`接口中的動態綁定方法是`draw()`,所以客戶程序員要做的是通過一個普通`Shape`引用調用`draw()`。`draw()`在所有派生類里都會被覆蓋。而且由于它是一個動態綁定方法,所以即使通過一個普通的`Shape`引用調用它,也有表現出正確的行為。這正是多態性的作用。
所以,我們一般創建一個特定的對象(`Circle`,`Square`,或者`Triangle`),把它向上轉換到一個`Shape`(忽略對象的特殊類型),以后便在程序的剩余部分使用匿名`Shape`引用。
作為對多態性和向上轉換的一個簡要回顧,可以象下面這樣為上述例子編碼(若執行這個程序時出現困難,請參考第3章3.1.2小節“賦值”):
```
//: Shapes.java
package c11;
import java.util.*;
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Circle.draw()");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Square.draw()");
}
}
class Triangle implements Shape {
public void draw() {
System.out.println("Triangle.draw()");
}
}
public class Shapes {
public static void main(String[] args) {
Vector s = new Vector();
s.addElement(new Circle());
s.addElement(new Square());
s.addElement(new Triangle());
Enumeration e = s.elements();
while(e.hasMoreElements())
((Shape)e.nextElement()).draw();
}
} ///:~
```
基類可編碼成一個`interface`(接口)、一個`abstract`(抽象)類或者一個普通類。由于`Shape`沒有真正的成員(亦即有定義的成員),而且并不在意我們創建了一個純粹的`Shape`對象,所以最適合和最靈活的表達方式便是用一個接口。而且由于不必設置所有那些`abstract`關鍵字,所以整個代碼也顯得更為清爽。
每個派生類都覆蓋了基類`draw`方法,所以具有不同的行為。在`main()`中創建了特定類型的`Shape`,然后將其添加到一個`Vector`。這里正是向上轉換發生的地方,因為`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`引用引用的準確類型是什么。
## 11.1.1 `Class`對象
為理解RTTI在Java里如何工作,首先必須了解類型信息在運行期是如何表示的。這時要用到一個名為“`Class`對象”的特殊形式的對象,其中包含了與類有關的信息(有時也把它叫作“元類”)。事實上,我們要用`Class`對象創建屬于某個類的全部“常規”或“普通”對象。
對于作為程序一部分的每個類,它們都有一個`Class`對象。換言之,每次寫一個新類時,同時也會創建一個`Class`對象(更恰當地說,是保存在一個完全同名的`.class`文件中)。在運行期,一旦我們想生成那個類的一個對象,用于執行程序的Java虛擬機(JVM)首先就會檢查那個類型的`Class`對象是否已經載入。若尚未載入,JVM就會查找同名的`.class`文件,并將其載入。所以Java程序啟動時并不是完全載入的,這一點與許多傳統語言都不同。
一旦那個類型的`Class`對象進入內存,就用它創建那一類型的所有對象。
若這種說法多少讓你產生了一點兒迷惑,或者并沒有真正理解它,下面這個示范程序或許能提供進一步的幫助:
```
//: SweetShop.java
// Examination of the way the class loader works
class Candy {
static {
System.out.println("Loading Candy");
}
}
class Gum {
static {
System.out.println("Loading Gum");
}
}
class Cookie {
static {
System.out.println("Loading Cookie");
}
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(
"After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
} ///:~
```
對每個類來說(`Candy`,`Gum`和`Cookie`),它們都有一個`static`從句,用于在類首次載入時執行。相應的信息會打印出來,告訴我們載入是什么時候進行的。在`main()`中,對象的創建代碼位于打印語句之間,以便偵測載入時間。
特別有趣的一行是:
```
Class.forName("Gum");
```
該方法是`Class`(即全部`Class`所從屬的)的一個`static`成員。而`Class`對象和其他任何對象都是類似的,所以能夠獲取和控制它的一個引用(裝載模塊就是干這件事的)。為獲得`Class`的一個引用,一個辦法是使用`forName()`。它的作用是取得包含了目標類文本名字的一個`String`(注意拼寫和大小寫)。最后返回的是一個`Class`引用。
該程序在某個JVM中的輸出如下:
```
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
```
可以看到,每個`Class`只有在它需要的時候才會載入,而`static`初始化工作是在類載入時執行的。
非常有趣的是,另一個JVM的輸出變成了另一個樣子:
```
Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
After Class.forName("Gum")
After creating Cookie
```
看來JVM通過檢查`main()`中的代碼,已經預測到了對`Candy`和`Cookie`的需要,但卻看不到`Gum`,因為它是通過對`forName()`的一個調用創建的,而不是通過更典型的`new`調用。盡管這個JVM也達到了我們希望的效果,因為確實會在我們需要之前載入那些類,但卻不能肯定這兒展示的行為百分之百正確。
(1) 類標記
在Java 1.1中,可以采用第二種方式來產生`Class`對象的引用:使用“類標記”。對上述程序來說,看起來就象下面這樣:
```
Gum.class;
```
這樣做不僅更加簡單,而且更安全,因為它會在編譯期間得到檢查。由于它取消了對方法調用的需要,所以執行的效率也會更高。
類標記不僅可以應用于普通類,也可以應用于接口、數組以及基本數據類型。除此以外,針對每種基本數據類型的包裝器類,它還存在一個名為`TYPE`的標準字段。`TYPE`字段的作用是為相關的基本數據類型產生`Class`對象的一個引用,如下所示:
| …… | 等價于…… |
| --- | --- |
| 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 |
## 11.1.2 轉換前的檢查
迄今為止,我們已知的RTTI形式包括:
(1) 經典轉換,如`(Shape)`,它用RTTI確保轉換的正確性,并在遇到一個失敗的轉換后產生一個`ClassCastException`異常。
(2) 代表對象類型的`Class`對象。可查詢`Class`對象,獲取有用的運行期資料。
在C++中,經典的`(Shape)`轉換并不執行RTTI。它只是簡單地告訴編譯器將對象當作新類型處理。而Java要執行類型檢查,這通常叫作“類型安全”的向下轉換。之所以叫“向下轉換”,是由于類分層結構的歷史排列方式造成的。若將一個`Circle`(圓)轉換到一個`Shape`(幾何形狀),就叫做向上轉換,因為圓只是幾何形狀的一個子集。反之,若將`Shape`轉換至`Circle`,就叫做向下轉換。然而,盡管我們明確知道`Circle`也是一個`Shape`,所以編譯器能夠自動向上轉換,但卻不能保證一個`Shape`肯定是一個`Circle`。因此,編譯器不允許自動向下轉換,除非明確指定一次這樣的轉換。
RTTI在Java中存在三種形式。關鍵字`instanceof`告訴我們對象是不是一個特定類型的實例(`Instance`即“實例”)。它會返回一個布爾值,以便以問題的形式使用,就象下面這樣:
```
if(x instanceof Dog)
((Dog)x).bark();
```
將`x`轉換至一個`Dog`前,上面的`if`語句會檢查對象`x`是否從屬于`Dog`類。進行轉換前,如果沒有其他信息可以告訴自己對象的類型,那么`instanceof`的使用是非常重要的——否則會得到一個`ClassCastException`異常。
我們最一般的做法是查找一種類型(比如要變成紫色的三角形),但下面這個程序卻演示了如何用`instanceof`標記出所有對象。
```
//: PetCount.java
// Using instanceof
package c11.petcount;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
public class PetCount {
static String[] typenames = {
"Pet", "Dog", "Pug", "Cat",
"Rodent", "Gerbil", "Hamster",
};
public static void main(String[] args) {
Vector pets = new Vector();
try {
Class[] petTypes = {
Class.forName("c11.petcount.Dog"),
Class.forName("c11.petcount.Pug"),
Class.forName("c11.petcount.Cat"),
Class.forName("c11.petcount.Rodent"),
Class.forName("c11.petcount.Gerbil"),
Class.forName("c11.petcount.Hamster"),
};
for(int i = 0; i < 15; i++)
pets.addElement(
petTypes[
(int)(Math.random()*petTypes.length)]
.newInstance());
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
catch(ClassNotFoundException e) {}
Hashtable h = new Hashtable();
for(int i = 0; i < typenames.length; i++)
h.put(typenames[i], new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.elementAt(i);
if(o instanceof Pet)
((Counter)h.get("Pet")).i++;
if(o instanceof Dog)
((Counter)h.get("Dog")).i++;
if(o instanceof Pug)
((Counter)h.get("Pug")).i++;
if(o instanceof Cat)
((Counter)h.get("Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get("Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get("Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get("Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(
pets.elementAt(i).getClass().toString());
for(int i = 0; i < typenames.length; i++)
System.out.println(
typenames[i] + " quantity: " +
((Counter)h.get(typenames[i])).i);
}
} ///:~
```
在Java 1.0中,對`instanceof`有一個比較小的限制:只可將其與一個已命名的類型比較,不能同`Class`對象作對比。在上述例子中,大家可能覺得將所有那些`instanceof`表達式寫出來是件很麻煩的事情。實際情況正是這樣。但在Java 1.0中,沒有辦法讓這一工作自動進行——不能創建`Class`的一個`Vector`,再將其與之比較。大家最終會意識到,如編寫了數量眾多的`instanceof`表達式,整個設計都可能出現問題。
當然,這個例子只是一個構想——最好在每個類型里添加一個`static`數據成員,然后在構造器中令其自增,以便跟蹤計數。編寫程序時,大家可能想象自己擁有類的源碼控制權,能夠自由改動它。但由于實際情況并非總是這樣,所以RTTI顯得特別方便。
(1) 使用類標記
`PetCount.java`示例可用Java 1.1的類標記重寫一遍。得到的結果顯得更加明確易懂:
```
//: PetCount2.java
// Using Java 1.1 class literals
package c11.petcount2;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
public class PetCount2 {
public static void main(String[] args) {
Vector pets = new Vector();
Class[] petTypes = {
// Class literals work in Java 1.1+ only:
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Offset by one to eliminate Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.addElement(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
Hashtable h = new Hashtable();
for(int i = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),
new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.elementAt(i);
if(o instanceof Pet)
((Counter)h.get(
"class c11.petcount2.Pet")).i++;
if(o instanceof Dog)
((Counter)h.get(
"class c11.petcount2.Dog")).i++;
if(o instanceof Pug)
((Counter)h.get(
"class c11.petcount2.Pug")).i++;
if(o instanceof Cat)
((Counter)h.get(
"class c11.petcount2.Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get(
"class c11.petcount2.Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get(
"class c11.petcount2.Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get(
"class c11.petcount2.Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(
pets.elementAt(i).getClass().toString());
Enumeration keys = h.keys();
while(keys.hasMoreElements()) {
String nm = (String)keys.nextElement();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" quantity: " + cnt.i);
}
}
} ///:~
```
在這里,`typenames`(類型名)數組已被刪除,改為從`Class`對象里獲取類型名稱。注意為此而額外做的工作:例如,類名不是`Getbil`,而是`c11.petcount2.Getbil`,其中已包含了包的名字。也要注意系統是能夠區分類和接口的。
也可以看到,`petTypes`的創建模塊不需要用一個`try`塊包圍起來,因為它會在編譯期得到檢查,不會象`Class.forName()`那樣“拋”出任何異常。
`Pet`動態創建好以后,可以看到隨機數字已得到了限制,位于1和`petTypes.length`之間,而且不包括零。那是由于零代表的是`Pet.class`,而且一個普通的`Pet`對象可能不會有人感興趣。然而,由于`Pet.class`是`petTypes`的一部分,所以所有`Pet`(寵物)都會算入計數中。
(2) 動態的`instanceof`
Java 1.1為`Class`類添加了`isInstance`方法。利用它可以動態調用`instanceof`運算符。而在Java 1.0中,只能靜態地調用它(就象前面指出的那樣)。因此,所有那些煩人的`instanceof`語句都可以從`PetCount`例子中刪去了。如下所示:
```
//: PetCount3.java
// Using Java 1.1 isInstance()
package c11.petcount3;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
public class PetCount3 {
public static void main(String[] args) {
Vector pets = new Vector();
Class[] petTypes = {
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Offset by one to eliminate Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.addElement(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
Hashtable h = new Hashtable();
for(int i = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),
new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.elementAt(i);
// Using isInstance to eliminate individual
// instanceof expressions:
for (int j = 0; j < petTypes.length; ++j)
if (petTypes[j].isInstance(o)) {
String key = petTypes[j].toString();
((Counter)h.get(key)).i++;
}
}
for(int i = 0; i < pets.size(); i++)
System.out.println(
pets.elementAt(i).getClass().toString());
Enumeration keys = h.keys();
while(keys.hasMoreElements()) {
String nm = (String)keys.nextElement();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" quantity: " + cnt.i);
}
}
} ///:~
```
可以看到,Java 1.1的`isInstance()`方法已取消了對`instanceof`表達式的需要。此外,這也意味著一旦要求添加新類型寵物,只需簡單地改變`petTypes`數組即可;毋需改動程序剩余的部分(但在使用`instanceof`時卻是必需的)。
- 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 推薦讀物