40.java编程思想——运行期类型鉴定需要
欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51287619
运行期类型鉴定(RTTI)的概念初看非常简单——手上只有基础类型的一个句柄时,利用它判断一个对象的正确类型。
然而,对RTTI 的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问题正式摆上了桌面。
如何利用Java 在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI ,它假定我们已在编译和运行期拥有所有类型;另一种是Java1.1特有的“反射”机制,利用它可在运行期独立查找类信息。
例如下图1常规类型是Shape 类,而特别衍生出来的类型是Circle,Square 和Triangle。
是一个典型的类结构示意图,基础类位于顶部,衍生类向下延展。面向对象编程的基本目标是用大量代码控制基础类型(这里是Shape)的句柄,所以假如决定添加一个新类(比如Rhomboid ,从Shape 衍生),从而对程序进行扩展,那么不会影响到原来的代码。在这个例子中,Shape接口中的动态绑定方法是draw(),所以客户程序员要做的是通过一个普通Shape 句柄调用draw()。draw()在所有衍生类里都会被覆盖。而且由于它是一个动态绑定方法,所以即使通过一个普通的Shape 句柄调用它,也有表现出正确的行为。这正是多形性的作用。
一般创建一个特定的对象(Circle,Square,或者Triangle),把它上溯造型到一个Shape(忽略对象的特殊类型),以后便在程序的剩余部分使用匿名Shape 句柄。
1 代码
import java.util.*;
interface Shape {
void draw();
}
class Circleimplements Shape {
publicvoiddraw() {
System.out.println("Circle.draw()");
}
}
class Squareimplements Shape {
publicvoiddraw() {
System.out.println("Square.draw()");
}
}
class Triangleimplements Shape {
publicvoiddraw() {
System.out.println("Triangle.draw()");
}
}
publicclass Shapes {
publicstaticvoidmain(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();
}
} /// :~
2 执行输出
Circle.draw()
Square.draw()
Triangle.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)的常规信息上。只有这样,我们的代码才更易实现、理解以及修改。所以说多形性是面向对象程序设计的一个常规目标。
3 C l a s s 对象
为理解RTTI 在Java 里如何工作,首先必须了解类型信息在运行期是如何表示的。这时要用到一个名为“Class 对象”的特殊形式的对象,其中包含了与类有关的信息(有时也把它叫作“元类”)。事实上,我们要用Class 对象创建属于某个类的全部“常规”或“普通”对象。对于作为程序一部分的每个类,它们都有一个Class 对象。换言之,每次写一个新类时,同时也会创建一个Class 对象(更恰当地说,是保存在一个完全同名的.class 文件中)。在运行期,一旦我们想生成那个类的一个对象,用于执行程序的Java虚拟机(JVM)首先就会检查那个类型的Class 对象是否已经载入。若尚未载入,JVM 就会查找同名的.class 文件,并将其载入。所以Java 程序启动时并不是完全载入的,这一点与许多传统语言都不同。
一旦那个类型的Class 对象进入内存,就用它创建那一类型的所有对象。下面这个示范程序或许能提供进一步的帮助:
3.1 代码2
class Candy {
static {
System.out.println("Loading Candy");
}
}
class Gum {
static {
System.out.println("Loading Gum");
}
}
class Cookie {
static {
System.out.println("Loading Cookie");
}
}
publicclass SweetShop {
publicstaticvoidmain(String[]args){
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
Class.forName("Gum");
} catch (ClassNotFoundExceptione) {
e.printStackTrace();
}
System.out.println("After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
} /// :~
3.2 执行如下
insidemain
LoadingCandy
Aftercreating Candy
LoadingGum
AfterClass.forName("Gum")
LoadingCookie
Aftercreating 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
AfterClass.forName("Gum")
Loading Cookie
After creating Cookie
可以看到,每个Class 只有在它需要的时候才会载入,而static 初始化工作是在类载入时执行的。
另一个JVM 的输出变成了另一个样子:
Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
AfterClass.forName("Gum")
After creating Cookie
看来JVM 通过检查main()中的代码,已经预测到了对Candy 和Cookie 的需要,但却看不到Gum,因为它是通过对forName()的一个调用创建的,而不是通过更典型的new 调用。尽管这个JVM 也达到了我们希望的效果,因为确实会在我们需要之前载入那些类,但却不能肯定这儿展示的行为百分之百正确。
3.3 类标记
在Java 1.1 中,可以采用第二种方式来产生Class 对象的句柄:使用“类标记”。对上述程序来说,看起来就象下面这样:
Gum.class;
这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以执行的效率也会更高。
类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据类型的封装器类,它还存在一个名为TYPE 的标准字段。TYPE 字段的作用是为相关的基本数据类型产生Class
对象的一个句柄,如下图2所示:
4 造型前的检查
已知的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 标记出所有对象。
4.1 代码3
import java.util.*;
class Pet {
}
class Dogextends Pet {
}
class Pugextends Dog {
}
class Catextends Pet {
}
class Rodentextends Pet {
}
class Gerbilextends Rodent {
}
class Hamsterextends Rodent {
}
class Counter {
inti;
}
publicclass PetCount {
static String[]typenames = { "Pet","Dog","Pug","Cat","Rodent","Gerbil","Hamster", };
publicstaticvoidmain(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 (inti = 0; i < 15;i++)
pets.addElement(petTypes[(int) (Math.random()*petTypes.length)].newInstance());
} catch (InstantiationExceptione) {
} catch (IllegalAccessExceptione) {
} catch (ClassNotFoundExceptione) {
}
Hashtable h = new Hashtable();
for (inti = 0; i < typenames.length;i++)
h.put(typenames[i],new Counter());
for (inti = 0; i < pets.size(); i++) {
Object o =pets.elementAt(i);
if (oinstanceofPet)
((Counter) h.get("Pet")).i++;
if (oinstanceofDog)
((Counter) h.get("Dog")).i++;
if (oinstanceofPug)
((Counter) h.get("Pug")).i++;
if (oinstanceofCat)
((Counter) h.get("Cat")).i++;
if (oinstanceofRodent)
((Counter) h.get("Rodent")).i++;
if (oinstanceofGerbil)
((Counter) h.get("Gerbil")).i++;
if (oinstanceofHamster)
((Counter) h.get("Hamster")).i++;
}
for (inti = 0; i < pets.size(); i++)
System.out.println(pets.elementAt(i).getClass().toString());
for (inti = 0; i < typenames.length;i++)
System.out.println(typenames[i] +" quantity: "+ ((Counter) h.get(typenames[i])).i);
}
} /// :~
4.2 输出
Petquantity: 0
Dogquantity: 0
Pugquantity: 0
Catquantity: 0
Rodentquantity: 0
Gerbilquantity: 0
Hamsterquantity: 0
在Java 1.0 中,对instanceof 有一个比较小的限制:只可将其与一个已命名的类型比较,不能同Class对象作对比。在上述例子中,大家可能觉得将所有那些instanceof 表达式写出来是件很麻烦的事情。实际情况正是这样。但在Java 1.0 中,没有办法让这一工作自动进行——不能创建Class 的一个Vector,再将其与之比较。如编写了数量众多的instanceof 表达式,整个设计都可能出现问题。当然,这个例子只是一个构想——最好在每个类型里添加一个static 数据成员,然后在构建器中令其增值,以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并非总是这样,所以RTTI 显得特别方便。
5 使用类标记
PetCount.java 示例可用Java 1.1 的类标记重写一遍。得到的结果显得更加明确易懂:
5.1 代码4
import java.util.*;
class Pet {
}
class Dogextends Pet {
}
class Pugextends Dog {
}
class Catextends Pet {
}
class Rodentextends Pet {
}
class Gerbilextends Rodent {
}
class Hamsterextends Rodent {
}
class Counter {
inti;
}
publicclass PetCount2 {
publicstaticvoidmain(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 (inti = 0; i < 15;i++) {
// Offset by one to eliminate Pet.class:
intrnd = 1 + (int)(Math.random() * (petTypes.length- 1));
pets.addElement(petTypes[rnd].newInstance());
}
} catch (InstantiationExceptione) {
} catch (IllegalAccessExceptione) {
}
Hashtable h = new Hashtable();
for (inti = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),new Counter());
for (inti = 0; i < pets.size(); i++) {
Object o =pets.elementAt(i);
if (oinstanceofPet)
((Counter) h.get("class c11.petcount2.Pet")).i++;
if (oinstanceofDog)
((Counter) h.get("class c11.petcount2.Dog")).i++;
if (oinstanceofPug)
((Counter) h.get("class c11.petcount2.Pug")).i++;
if (oinstanceofCat)
((Counter) h.get("class c11.petcount2.Cat")).i++;
if (oinstanceofRodent)
((Counter) h.get("class c11.petcount2.Rodent")).i++;
if (oinstanceofGerbil)
((Counter) h.get("class c11.petcount2.Gerbil")).i++;
if (oinstanceofHamster)
((Counter) h.get("class c11.petcount2.Hamster")).i++;
}
for (inti = 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);
}
}
} /// :~
5.2 输出
typenames(类型名)数组已被删除,改为从Class对象里获取类型名称。注意为此而额外做的工作:例如,类名不是Getbil,而是c11.petcount2.Getbil,其中已包含了包的名字。也要注意系统是能够区分类和接口的。
可以看到,petTypes 的创建模块不需要用一个try 块包围起来,因为它会在编译期得到检查,不会象Class.forName()那样“掷”出任何违例。
Pet 动态创建好以后,可以看到随机数字已得到了限制,位于1 和petTypes.length 之间,而且不包括零。那是由于零代表的是Pet.class,而且一个普通的Pet 对象可能不会有人感兴趣。然而,由于Pet.class 是petTypes 的一部分,所以所有Pet(宠物)都会算入计数中。
5.3 代码5
Java 1.1 为Class 类添加了isInstance 方法。利用它可以动态调用instanceof 运算符。而在Java 1.0中,只能静态地调用它(就象前面指出的那样)。因此,所有那些烦人的instanceof语句都可以从PetCount 例子中删去了。如下所示:
import java.util.*;
class Pet {
}
class Dogextends Pet {
}
class Pugextends Dog {
}
class Catextends Pet {
}
class Rodentextends Pet {
}
class Gerbilextends Rodent {
}
class Hamsterextends Rodent {
}
class Counter {
inti;
}
publicclass PetCount3 {
publicstaticvoidmain(String[]args){
Vector pets = new Vector();
Class[] petTypes = { Pet.class, Dog.class, Pug.class, Cat.class, Rodent.class, Gerbil.class, Hamster.class, };
try {
for (inti = 0; i < 15;i++) {
// Offset by one to eliminate Pet.class:
intrnd = 1 + (int)(Math.random() * (petTypes.length- 1));
pets.addElement(petTypes[rnd].newInstance());
}
} catch (InstantiationExceptione) {
} catch (IllegalAccessExceptione) {
}
Hashtable h = new Hashtable();
for (inti = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),new Counter());
for (inti = 0; i < pets.size(); i++) {
Object o =pets.elementAt(i);
// Using isInstance to eliminate individual
// instanceof expressions:
for (intj = 0; j < petTypes.length; ++j)
if (petTypes[j].isInstance(o)) {
String key = petTypes[j].toString();
((Counter) h.get(key)).i++;
}
}
for (inti = 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);
}
}
} /// :~
5.4 输出
classHamster
classPug
classRodent
classDog
classGerbil
classDog
classDog
classGerbil
classGerbil
classDog
classRodent
classRodent
classCat
classDog
classRodent
classHamster quantity: 1
classGerbil quantity: 3
classCat quantity: 1
classPet quantity: 15
classPug quantity: 1
classRodent quantity: 8
classDog quantity: 6
可以看到,Java 1.1 的isInstance()方法已取消了对instanceof 表达式的需要。此外,这也意味着一旦要求添加新类型宠物,只需简单地改变petTypes 数组即可;毋需改动程序剩余的部分(但在使用instanceof时却是必需的)。