第十章类型检查运行期类型识别(RTTI,run-timetypeidentification)的概念初看起来非常简单:当你只有一个指向对象的基类的引用时,RTTI机制可以让你找出这个对象确切的类型。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的问题,同时也提出了如何组织程序的问题。本章将讨论Java是如何允许我们在运行期识别对象和类的信息。主要有两种方式:一种是传统的RTTI,它假定我们在编译期和运行期已经知道了所有的类型;另一种是“反射机制(reflection)”,它允许我们在运行期获得类的信息。我们先讨论“传统”的RTTI,再讨论反射。为什么需要RTTI让我们来思考已经很熟悉了的一个使用了多态的类层次结构的例子。最一般化的类型是基类Shape,而派生出的具体类有Circle,Square和Triangle。这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程基本的目的是:你的代码只操纵对基类(这里是Shape)的引用。这样,如果你要添加一个新类(比如从Shape派生Rhomboid)来扩展程序,就不会影响到原来的代码。在这个例子的Shape接口中动态绑定了draw()方法,目的就是让客户端程序员使用一般化的Shape的引用来调用draw()。draw()在所有派生类里都会被重载,并且由于它是被动态绑定的,所以即使是通过通用的Shape引用来调用,也能产生正确行为。这就是多态(polymorphism)。因此,我们通常会创建一个特定的对象(Circle,Square,或者Triangle),把它向上转型成Shape(忽略对象的特定类型),并在后面的程序中使用匿名(译注:即不知道具体类型)的Shape引用。简要复习一下多态和向上类型转换,并为上面的例子编码://:c10:Shapes.javaimportcom.bruceeckel.simpletest.*;classShape{voiddraw(){System.out.println(this+.draw());}}classCircleextendsShape{publicStringtoString(){returnCircle;}}classSquareextendsShape{publicStringtoString(){returnSquare;}}classTriangleextendsShape{publicStringtoString(){returnTriangle;}}publicclassShapes{privatestaticTestmonitor=newTest();publicstaticvoidmain(String[]args){//ArrayofObject,notShape:Object[]shapeList={newCircle(),newSquare(),newTriangle()};for(inti=0;ishapeList.length;i++)((Shape)shapeList[i]).draw();//Mustcastmonitor.expect(newString[]{Circle.draw(),Square.draw(),Triangle.draw()});}}///:~基类中包含draw()方法,它通过传递this参数给System.out.println(),间接地使用toString()打印类标识符。如果是某个对象调用这个方法,它会自动调用toString()生成字符串。每个派生类都要重载(从Object类继承来的)toString()方法,这样draw()在不同情况下就打印出不同的消息。在main()中,生成了各种特定类型的Shape,并加入到数组中。这个数组有点特别,因为它不是一个Shape的数组(虽然它可以是),而是根类Object类的对象的数组。这样做的原因是为第十一章作准备,我们将学习collection工具(也被称作容器container),它唯一的工作是保存与管理对象。基于通用性的考虑,这些collection应该能保存任何类型的对象,因此它们保存根类Object类的对象。Object类的数组引出了我们将在第十一章Collection中学习的一个重要的问题。在这个例子中,当把Shape对象放入Object类的数组时会向上转型。由于在Java中所有的对象都是根类Object类的对象(除了基本类型),所以一个Object的数组自然能保存Shape类的对象。但在向上类型转换为Object的时候也失去了作为Shape对象特定的信息。对于这个数组,它们就只是Object的对象。当你通过索引操作符从数组中取出一个元素时,就要多做一些事情了。由于数组保存的只能是Object对象,因此通过索引获得的也只是Object对象的引用。但我们知道那其实是Shape对象的引用,而且我们想给这个对象发送Shape对象能够接收的消息。所以必须使用传统的“(Shape)”方式显式地将Object对象的引用转换成Shape对象的引用。这是RTTI最基本的使用形式,因为在Java中,所有的类型转换都是在运行期检查的。这也是RTTI名字的来源:在运行期间,识别一个对象的类型。在这个例子中,RTTI类型转换并不彻底:Object被转型为Shape,而不是转型为Circle,Square,或者Triangle。这是因为目前我们只知道这个数组保存的都是Shape。在编译时刻,这只能由你自己设定的规则来强制确保这一点,而在运行时刻,由类型转换操作来确保这一点。接下来就是多态机制的事情了,Shape对象实际上执行什么样的代码,是由引用指向的具体对象是Circle,Square或者Triangle而决定的。通常这正是它应该执行的行为;希望你的大部分代码尽可能少的了解对象特定的类型,而是只与一个对象家族中的通用表示打交道(在这个例子中是Shape)。这样你的代码会更容易写,更容易读,并更便于维护,你的设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。但是,假如你碰到了一个特殊的编程问题,如果你能够知道某个引用得确切类型,就可以使用最简单的方式去解决它,那么此时你又该怎么办呢?例如,假设我们允许用户将某一具体类型的几何形状全都变成紫色,以突出显示它们。通过这种方法,用户就能找出屏幕上所有被突出显示的三角形。或者,你的方法可能被用来旋转列表中的所有图形,但你想跳过圆形,因为对圆形作旋转没有意义。使用RTTI,你可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特例。Class对象要理解RTTI在Java中是如何工作的,首先必须要知道类型信息在运行期是如何表示的。这项工作是由被称为“Class对象”的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象正是被用来创建类的“常规”对象的。作为程序一部分,每个类都有一个Class对象。换言之,每当你编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。在运行期,一旦我们想生成这个类的一个对象,运行这个程序的Java虚拟机(JVM)首先检查这个类的Class对象是否已经加载。如果尚未加载,JVM就会根据类名查找.class文件,并将其载入。所以Java程序并不是一开始执行,就被完全加载的,这一点与许多传统语言都不同。一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。如果这么解释仍然不清楚,或者你并不相信,下面的示范程序可以证明我的说法://:c10:SweetShop.java//Examinationofthewaytheclassloaderworks.importcom.bruceeckel.simpletest.*;classCandy{static{System.out.println(LoadingCandy);}}classGum{static{System.out.println(LoadingGum);}}classCookie{static{System.out.println(LoadingCookie);}}publicclassSweetShop{privatestaticTestmonitor=newTest();publicstaticvoidmain(String[]args){System.out.println(insidemain);newCandy();System.out.println(AftercreatingCandy);try{Class.forName(Gum);}catch(ClassNotFoundExceptione){System.out.println(Couldn'tfindGum);}System.out.println(AfterClass.forName(\Gum\));newCookie();System.out.println(AftercreatingCookie);monitor.expect(newString[]{insidemain,LoadingCandy,AftercreatingCandy,LoadingGum,AfterClass.forName(\Gum\),LoadingCookie,AftercreatingCookie});}}///:~这里的每个类Candy,Gum和Cookie中,都有一个static语句,在类第一次被加载时执行,这是会有相应的信息打印出来,告诉我们这个类什么时候被加载了。在main()中,创建对象的代码被置于打印语句之间,以帮助我们判断加载的时间点。你可以从输出中看到,Class对象仅在需要的时候才被加载,static语句块是在类加载时被执行的。特别有趣的一行是:Class.forName(Gum);这是Class类(所有Class对象都属于这个类型)的一个static成员。Class对象就和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象的引用的一种方法。它是用一个包含目标类的文本名(注意拼写和大小写)的String作输入参数,返回的是一个Class对象的引用,上面的代码忽略了返回值。对forName()的调用是为了它产生的“副作用”:如果类Gum还没有被加载就加载它。在加载的过程中,Gum的static语句被执行。在前面的例子里,如果Class.forName()找不到你要加载的类,它会抛出异常ClassNotFoundException(理想状况下,异常的名字就能告诉你出了什么问题)。这里我们只需简单报告问题,但在更严密的程序里,你可能要在异常处理程序中解决这个问题。类字面常量(Classliteral)Java还提供了另一种方法来生成Class对象的引用:使用“类字面常量(classliteral)”。对上述程序来说,看起来就象下面这样:Gum.class;这样做不仅更简单,而且更安全,因为它在编译期就会受到检查。并且它无需方法调用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。以外,对于基本数据类型的包装类,还有一个标准域TYPE。TYPE域是一个引用,指向对应的基本数据类型的Class对象,如下所示:...等价于...boolean.classBoolean.TYPEchar.classCharacter.TYPEbyte.classByte.TYPEshort.classShort.TYPEint.classInteger.TYPElong.classLong.TYPEfloat.classFloat.TYPEdouble.classDouble.TYPEvoid.classVoid.TYPE我建议使用”.class”的形式,以保持它们与常规类的一致性。类型转换前先作检查迄今为止,我们已知的RTTI形式包括:1.经典