Thinking in Java 3 Edition 10:检测类型 初看起来“运行时类型识别( run-time type identification,缩写为 RTT)”的想法很简单:要让你在只持有这个对象的基类的 reference的 情况下,找出它的确切的类型。 但是,这种对“RTTI的强烈需求”揭示了许多OO设计中会碰到的有趣 (同时也很令人困惑)的问题,并且引出了“该如何组织程序结构”这一根 本性的问题 本章要讲解Java所提供的,让你在运行时发现对象和类的信息的方法。 它有两种形式:一种是假设你在编译和运行时都完全知道类型的具体信息 的“传统的”RTT,另一种是允许你只依靠运行时信息发掘类型信息的 “ reflection”机制。我们先讲“传统的”RTTI,再讨论 reflection。 为什么会需要RTTT 先想一想我们现在已经很熟悉的那个多态性的例子。通用的基类 Shape 派生出具体的 Circle, Square和 Triangle类: Shape draw Circle square Triangle 这是一个典型的类系( class hierarchy)结构图,基类在最上面,派生类 在下面。OOP的目的就是要让你用基类的 reference(这里就是 Shape) 来写程序,因此如果你往程序里面加了一个新的类(比如 Shape又派生 了一个 Rhomboid),绝大多数的代码根本不会受到影响。在这个例子 里面, Shape接口动态绑定的是draw(),于是客户程序员能够通过 通用的 Shape reference来调用draw()。所有派生类都会覆写 draw(),而且由于这个方法是动态绑定的,因此即便是通过通用的 Shape reference来进行的调用,也能得到正确的行为。这就是多态 性 由此,编程时一般会先创建一个具体的对象( Circle, Square,或 Triangle),再把它上传到 Shape(这样就把对象的具体类型给忘了) 然后在余下来的程序里面使用那个匿名的 Shape reference 第1页共17页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition 第 1 页 共 17 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 10: 检测类型 初看起来“运行时类型识别(run-time type identification,缩写为 RTTI)”的想法很简单:要让你在只持有这个对象的基类的 reference 的 情况下,找出它的确切的类型。 但是,这种对“RTTI 的强烈需求”揭示了许多 OO 设计中会碰到的有趣 (同时也很令人困惑)的问题,并且引出了“该如何组织程序结构”这一根 本性的问题。 本章要讲解 Java 所提供的,让你在运行时发现对象和类的信息的方法。 它有两种形式:一种是假设你在编译和运行时都完全知道类型的具体信息 的“传统的”RTTI,另一种是允许你只依靠运行时信息发掘类型信息的 “reflection”机制。我们先讲“传统的”RTTI,再讨论 reflection。 为什么会需要 RTTI 先想一想我们现在已经很熟悉的那个多态性的例子。通用的基类 Shape 派生出具体的 Circle,Square 和 Triangle 类: 这是一个典型的类系(class hierarchy)结构图,基类在最上面,派生类 在下面。OOP 的目的就是要让你用基类的 reference(这里就是 Shape) 来写程序,因此如果你往程序里面加了一个新的类(比如 Shape 又派生 了一个 Rhomboid),绝大多数的代码根本不会受到影响。在这个例子 里面,Shape 接口动态绑定的是 draw( ),于是客户程序员能够通过 通用的 Shape reference 来调用 draw( )。所有派生类都会覆写 draw( ),而且由于这个方法是动态绑定的,因此即便是通过通用的 Shape reference 来进行的调用,也能得到正确的行为。这就是多态 性。 由此,编程时一般会先创建一个具体的对象(Circle,Square,或 Triangle),再把它上传到 Shape(这样就把对象的具体类型给忘了), 然后在余下来的程序里面使用那个匿名的 Shape reference
Chapter 10: Detecting Types 作为多态性的简要回顾,我们再用程序来描述一遍上面这个例子 //: c10: Shapes. java import com. bruceeckel. simpletest. c⊥ass Shape void draw()[ System. out. println(this draw()");} class Circle extends Shape public String tostring() return Circle"i) class Square extends Shape public string tostring()i return Square"i J class Triangle extends shape i public String tostring ()( return "Triangl public class Shapes i private static Test monitor new Test()i public static void main(String[ args)I // Array of object, not shape Object[ shapelist = t new circled), () new Triangle( for(int 1=0 Shape)shapelist[i]).draw()i// Must cast monitor. expect(new String[] t le draw( 'Square. draw () Triangle. draw( 基类里有一个间接调用 tostring()来打印类的打印标识符的draw() 方法。它把this传给 System. out println(),而这个方法一看到对 象就会自动调用它的 tostring()方法,这样就能生成对象的 String表 达形式了。每个派生类都会覆写 tostring()方法(从 Object继承下来 的),这样draw()就能(多态地)打印各种对象了 man()创建了一些具体的 Shape对象并且把它们加进一个数组。这个 数组有点怪,因为它不是 Shape(尽管可以这样),而是根类 Object的 数组。这是在为第11章要讲的 collection(也称 container)作铺垫。 collection是一种工具,它只有一种用途,就是要为你保管其它对象。因 此出于通用性的考虑,这些 collection应该能持有任何东西。所以它们持 第2页共17页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Chapter 10: Detecting Types www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 2 页 共 17 页 作为多态性的简要回顾,我们再用程序来描述一遍上面这个例子: //: c10:Shapes.java import com.bruceeckel.simpletest.*; class Shape { void draw() { System.out.println(this + ".draw()"); } } class Circle extends Shape { public String toString() { return "Circle"; } } class Square extends Shape { public String toString() { return "Square"; } } class Triangle extends Shape { public String toString() { return "Triangle"; } } public class Shapes { private static Test monitor = new Test(); public static void main(String[] args) { // Array of Object, not Shape: Object[] shapeList = { new Circle(), new Square(), new Triangle() }; for(int i = 0; i < shapeList.length; i++) ((Shape)shapeList[i]).draw(); // Must cast monitor.expect(new String[] { "Circle.draw()", "Square.draw()", "Triangle.draw()" }); } } ///:~ 基类里有一个间接调用 toString( )来打印类的打印标识符的 draw( ) 方法。它把 this 传给 System.out.println( ),而这个方法一看到对 象就会自动调用它的 toString( )方法,这样就能生成对象的 String 表 达形式了。每个派生类都会覆写 toString( )方法(从 Object 继承下来 的),这样 draw( )就能(多态地)打印各种对象了。 main( )创建了一些具体的 Shape 对象并且把它们加进一个数组。这个 数组有点怪,因为它不是 Shape(尽管可以这样),而是根类 Object 的 数组。这是在为第 11 章要讲的 collection(也称 container)作铺垫。 collection 是一种工具,它只有一种用途,就是要为你保管其它对象。因 此出于通用性的考虑,这些 collection 应该能持有任何东西。所以它们持
Thinking in Java 3 Edition 有 Object。因此你可以从 Object数组看到一个你将在第11章遇到的 重要问题 在这个例子里,当把 Shape放进 object数组的时候,上传就发生了。 由于Java里面的所有东西(除了 primitive)都是对象,因此 Object数 组也能持有 Shape对象。但是在上传过程中,“这个对象是 Shape 的信息丢失了。对数组而言,它们都只是 object 但是,当你用下标把元素从数组里提取出来的时候,就免不了要忙活一阵 了。由于数组持有的都是些 Object,因此提取出来的也就自然都是 Object的 reference了。但是我们知道,它实际上是 Shape的 reference,而且我们也要向它发送 Shape的消息。于是老套的 “( Shape)”类型转换就变得必不可少了。这是RTTI的最基本的形 式,因为程序运行的时候会逐项检査转换是不是都正确。这种检查正体现 了RTTI的本义:在运行时鉴别对象的类型 这里的RTT转换并不彻底: object只是转换成 Shape,而不是一下 子转换成 Circle, Square或 Triangle。这是因为现阶段我们只知道 数组存储的是 Shape。编译的时候,这种转换要由你自己来把关,但是 运行的时候这种转换就不用你操心了。 接下来就交给多态性了。 Shape到底要执行哪段代码,这要由 reference究竟是指向 Circle, Square还是 Triangle对象来决定。 总之程序就该这么写;你的目标就是,要让绝大多数代码只同代表这个类 系的基类对象打交道(在本案中,就是 Shape),对它们来说,越少知道 下面的具体类型越好。这样一来,代码的读写和维护就会变得更简单,而 实现,理解和修改设计方案也会变得更容易一些。所以多态性是OOP的 个主要目标 但是如果你碰到一个特殊问题,而要解决这个问题,最简单的办法就是 “找出这个通用的 reference究竟是指哪个具体类型的”,那你又该怎 么办呢?比方说,你要让用户能用“把这类对象全都染成紫色”的办法来 突出某种 Shape。或者,你要写一个会用到“旋转”一组 shape的方 法。但是对圆形来说,旋转是没有意义的,所以你只想跳过圆形对象。有 了RTTI,你就能问出 Shape的 reference究竟是指的哪种具体类型的 对象了,这样你就能把特例给区别出来了。 Class对象 想要知道Java的RTTI是如何工作的,你就必须首先知道程序运行的时 候,类型信息是怎样表示的。这是由一种特殊的,保存类的信息的,叫作 “Cass对象( Class object)”的对象来完成的。实际上类的“常规”对 象是由 Class对象创建的 第3页共17页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition 第 3 页 共 17 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 有 Object。因此你可以从 Object 数组看到一个你将在第 11 章遇到的 重要问题。 在这个例子里,当把 Shape 放进 Object 数组的时候,上传就发生了。 由于 Java 里面的所有东西(除了 primitive)都是对象,因此 Object 数 组也能持有 Shape 对象。但是在上传过程中,“这个对象是 Shape” 的信息丢失了。对数组而言,它们都只是 Object。 但是,当你用下标把元素从数组里提取出来的时候,就免不了要忙活一阵 了。由于数组持有的都是些 Object,因此提取出来的也就自然都是 Object 的 reference 了。但是我们知道,它实际上是 Shape 的 reference,而且我们也要向它发送 Shape 的消息。于是老套的 “(Shape)”类型转换就变得必不可少了。这是 RTTI 的最基本的形 式,因为程序运行的时候会逐项检查转换是不是都正确。这种检查正体现 了 RTTI 的本义:在运行时鉴别对象的类型。 这里的 RTTI 转换并不彻底:Object 只是转换成 Shape,而不是一下 子转换成 Circle,Square 或 Triangle。这是因为现阶段我们只知道 数组存储的是 Shape。编译的时候,这种转换要由你自己来把关,但是 运行的时候这种转换就不用你操心了。 接下来就交给多态性了。Shape 到底要执行哪段代码,这要由 reference 究竟是指向 Circle,Square 还是 Triangle 对象来决定。 总之程序就该这么写;你的目标就是,要让绝大多数代码只同代表这个类 系的基类对象打交道(在本案中,就是 Shape),对它们来说,越少知道 下面的具体类型越好。这样一来,代码的读写和维护就会变得更简单,而 实现,理解和修改设计方案也会变得更容易一些。所以多态性是 OOP 的 一个主要目标。 但是如果你碰到一个特殊问题,而要解决这个问题,最简单的办法就是 “找出这个通用的 reference 究竟是指哪个具体类型的”,那你又该怎 么办呢?比方说,你要让用户能用“把这类对象全都染成紫色”的办法来 突出某种 Shape。或者,你要写一个会用到“旋转”一组 shape 的方 法。但是对圆形来说,旋转是没有意义的,所以你只想跳过圆形对象。有 了 RTTI,你就能问出 Shape 的 reference 究竟是指的哪种具体类型的 对象了,这样你就能把特例给区别出来了。 Class 对象 想要知道 Java 的 RTTI 是如何工作的,你就必须首先知道程序运行的时 候,类型信息是怎样表示的。这是由一种特殊的,保存类的信息的,叫作 “Class 对象 (Class object)”的对象来完成的。实际上类的“常规”对 象是由 Class 对象创建的
Chapter 10: Detecting Types 程序里的每个类都要有一个clas对象。也就是说,每次你撰写并且编 译了一个新的类的时候,你就创建了一个新的 class对象(而且可以这么 说,这个对象会存储在同名的 class文件里)。程序运行时,当你需要创 建一个那种类的对象的时候,巩M会检查它是否装载了那个 Class对 象。如果没有,巩VM就会去找那个 class文件,然后装载。由此也可知 道,Java程序在启动的时候并没有完全装载,这点同许多传统语言是不 样的。 那种类型的 class对象被装进了内存,所有那个类的对象就都会由 它来创建了。如果这听上去太玄,或者你不相信的话,下面这个程序就能 作证明 77: c10: Sweet shop. java Examination of the way the class loader works import com. bruceeckel. simpletest. class Candy static I System. out. println("Loading Candy") class Gum I System. out. println("Loading Gum") class Cookie i static System. out. println("Loading Cookie")i public class Sweet shop t private static Test monitor new Test()i public static void main(string[] args)t System. out. println("inside main")i new Candy ()i System. out. printin ("After creating candy" )i System. out. println("Couldn't find Gum")i System. out. println(" After Class. f (\"Gum\")") System. out. println ("After creating Cookie monitor. expect (new String[] t Loading Gum", 第4页共17页 www.wgqqh.com/shhgs/tij.html
Chapter 10: Detecting Types www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 4 页 共 17 页 程序里的每个类都要有一个 Class 对象。也就是说,每次你撰写并且编 译了一个新的类的时候,你就创建了一个新的 Class 对象(而且可以这么 说,这个对象会存储在同名的.class 文件里)。程序运行时,当你需要创 建一个那种类的对象的时候,JVM 会检查它是否装载了那个 Class 对 象。如果没有,JVM 就会去找那个.class 文件,然后装载。由此也可知 道,Java 程序在启动的时候并没有完全装载,这点同许多传统语言是不 一样的。 一旦那种类型的 Class 对象被装进了内存,所有那个类的对象就都会由 它来创建了。如果这听上去太玄,或者你不相信的话,下面这个程序就能 作证明: //: c10:SweetShop.java // Examination of the way the class loader works. import com.bruceeckel.simpletest.*; 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 { private static Test monitor = new Test(); 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) { System.out.println("Couldn't find Gum"); } System.out.println("After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); monitor.expect(new String[] { "inside main", "Loading Candy", "After creating Candy", "Loading Gum", "After Class.forName(\"Gum\")", "Loading Cookie
Thinking in Java 3 Edition Candy,Gum,和 Cookie,每个类都有一条“会在类第一次被装载的 时候执行”的 static语句。这样装载的类时候就会它就会打印消息通知 你了。对象的创建被分散到了man()的各条打印语句之间,其目的就 是要帮助你检测装载的时机。 你可以从程序的输出看到, Class对象只会在需要的时候装载,而 static初始化则发生在装载类的时候。 下面这行特别有趣 Class. forName("Gum 这是一个 Class的 static方法(所有的 Class对象所共有的)。 class 对象同其它对象一样,也可以用 reference来操控(这是装载器要干的), 而要想获取其 reference, forName()就是一个办法。它要一个表示 这个类的名字的 String作参数(一定要注意拼写和大小写!)。这个方法 会返回 class的 reference,不过程序里面没用到这个 reference;这 里只是要用它的副作用——看看Gum类装载了没有,要是还没有那就马 上装载。装载的过程中,程序执行了Gum的 static语句。 在上述例程中,如果 Class. forName()没有找到它要装载的类,就会 抛出一个 ClassNotFound Exception(太完美了!单从异常的名字你 就能知道出了什么问题了)。这里,我们只是简单地报告一下问题,然后 继续下去,但是在较为复杂的程序里,你或许应该在异常处理程序里把这 个问题解决掉。 Class常数 Java还提供了一种获取 Class对象的 reference的方法:“cass常数 ( class litera/)”。对于上述程序,它就是 Gum. class 这种写法不但更简单,而且也更安全,因为它是在编译时做检查的。此外 由于没有方法调用,它的执行效率也更高一些。 第5页共17页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition 第 5 页 共 17 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com "After creating Cookie" }); } } ///:~ Candy,Gum,和 Cookie,每个类都有一条“会在类第一次被装载的 时候执行”的 static 语句。这样装载的类时候就会它就会打印消息通知 你了。对象的创建被分散到了 main( )的各条打印语句之间,其目的就 是要帮助你检测装载的时机。 你可以从程序的输出看到,Class 对象只会在需要的时候装载,而 static 初始化则发生在装载类的时候。 下面这行特别有趣: Class.forName("Gum"); 这是一个 Class 的 static 方法(所有的 Class 对象所共有的)。Class 对象同其它对象一样,也可以用 reference 来操控(这是装载器要干的), 而要想获取其 reference,forName( )就是一个办法。它要一个表示 这个类的名字的 String 作参数(一定要注意拼写和大小写!)。 这个方法 会返回 Class 的 reference,不过程序里面没用到这个 reference;这 里只是要用它的副作用——看看 Gum 类装载了没有,要是还没有那就马 上装载。装载的过程中,程序执行了 Gum 的 static 语句。 在上述例程中,如果 Class.forName( )没有找到它要装载的类,就会 抛出一个 ClassNotFoundException(太完美了!单从异常的名字你 就能知道出了什么问题了)。这里,我们只是简单地报告一下问题,然后 继续下去,但是在较为复杂的程序里,你或许应该在异常处理程序里把这 个问题解决掉。 Class 常数 Java 还提供了一种获取 Class 对象的 reference 的方法:“class 常数 (class literal)”。对于上述程序,它就是: Gum.class; 这种写法不但更简单,而且也更安全,因为它是在编译时做检查的。此外 由于没有方法调用,它的执行效率也更高一些