10:检测类型
运行时类型识别(run-time type identification,缩写为RTTI)。
为什么会需要RTTI
collection是一种工具,它只有一种用途,就是要为你保管其它对象。因此出于通用性的考虑,这些collection应该能持有任何东西。所以它们持有Object。
Class对象
想要知道JAVA的RTTI是如何工作的,你就必须首先知道程序运行的时候,类型信息是怎样表示的。这是由一种特殊的,保存类的信息的,叫做“Class对象(Class object)”的对象来完成。实际上类的“常规”对象是由Class对象创建的。
程序里的每个类都要有一个Class对象。也就是说,每次你撰写并且编译了一个新的类的时候,你就创建了一个新的Class对象(而且可以这么说,这个对象会存储在同名的.class文件里)。程序运行时,当你需要创建一个那种类的对象的时候,JVM会检查它是否装载了那个Class对象。如果没有, JVM就会去找那个.class文件,然后装载。由此也可知道,Java程序在启动的时候并没有完全装载,这点同许多传统语言是不一样的。
Class.forName("一个类的名字");
这是一个Class的static方法(所有的Class对象所共有的)。Class对象同其它对象一样,也可以用reference来操控(这是装载器要干的),而要想获取其reference, forName()就是一个办法。它要一个表示这个类的名字的String作参数(一定要注意拼写喝大小写!)。这个方法会返回Class的 reference,还有一个副作用,看看这个String所说的那个类装载了没有,要是还没有那就马上装载。如果Class.forName()没有找到它要装载的类,就会抛出一个ClassNotFoundException。
Class常数
Java还提供了一种获取Class对象的reference的方法:“class常数(class literal)”。
类的名字.class;
这种写法不但更简单,而且也更安全,因为它是在编译时做检查的。此外由于没有方法调用,它的执行效率也更高一些。
Class常数不但能用于普通类,也可以用于接口,数组和primitive类型。此外,每种primitive的wrapper类还有一个标准的,名为 TYPE的数据成员。这个TYPE能返回“与这种primitive相关联的wrapper类”的Class对象的reference,就像这样:
... 等同于 ...
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
我喜欢尽量使用“.class”,因为这种写法与普通类的保持一致。
转换之前先作检查
到目前为止,你看到的RTTI的形式有:
1。经典的类型转换:如“(Shape)”,这种转换要经过RTTI的检查。要是做了错误的转换,它就会抛出ClassCastException。
2.代表对象类型的Class对象。你可以在运行的时候查询Class对象,以此来获取所需的信息。
如果不进行明确的类型转换的话,编译器时不会让你把对象赋给派生类的reference的。
Java里面还有第三种RTTI的形式。这就是instanceof关键词,它会告诉你对象是不是某个类的实例。它返回的是一个boolean值。
使用类常数
动态的instanceof
isInstance()能完全替代instanceof。
instanceof vs. Class的相等性
RTTI的语法
Class.getInterfaces()方法会返回一个Class对象的数组。数组中的对象分别表示它所实现的接口。
如果你手上有一个Class对象,你还能用getSuperclass()问出它最近的那个父类。当然,这会返回一个Class的reference,于是你可以接着问,程序运行的时候,你能以此发现对象的完整的关系。
Class的newInstance()方法就像是另一种clone()对象的方法。但是,你却可以用newInstance()凭空创建出一个新的对象。
printInfo()方法,它拿一个Class对象的reference作参数,用getName()提取类的名字,用isInterface()判断它是不是接口。这样,你就能仅凭Class对象就找出所有你想知道的这个对象的信息了。
Reflection:运行时的类信息
Java以JavaBeans的形式提供了基于组件的编程的支持。
通过网络在远程机器上创建对象并运行程序。这被成为“远程方法调用(Remote Method Invocation缩写是RMI)”。它能让一个Java程序将对象分布到很多机器上。
除了Class类,还有一个类库,java.lang.reflect也支持reflection。这个类库里面有Field,Method,和 Constructor类(它们都实现了Member接口)运行时,JVM会创建一些这种类的对象来代表未知类的各个成员。然后,你就能用 Constructor来创建新的对象,用get()和set()来读取和修改与Field队形爱女嘎相关联的成员数据,用invoke()方法调用与 Method对象相关联的方法了。此外,你还能用getFields(),getMethods(),getConstructors()之类的方法,获取表示成员数据,方法或构造函数的对象数组。由此,即便编译时什么信息都得不到,你也有办法能在运行时问出匿名对象的全部类型信息了。
有一点很重要,reflection不是什么妖术。当你用reflection与未知类的对象打交道的时候,JVM(会和普通的RTTI一样)先看看这个对象是属于那个具体类型的,但是此后,它还是得先装载Class对象才能工作。也就是,不管是从本地还是从网络,反正JVM必须拿到那个.class文件。所以RTTI同reflection的真正区别在于,RTTI是在编译时让编译器打开并且检查.class文件。换句话说,你是在通过“正常”途径调用对象的方法。而对reflection来说,编译时是得不到.class文件的;所以它是在运行时打开并检查那个文件。
一个提取类的方法的程序
一般来说,你不太会直接使用reflection;Java之所以要有这种功能是要用它来支持一些憋的特性,比如对象的序列化和JavaBeans。不过在有些情况下,能动态提取类的信息还是很有用的。
Class的getMethods()和getConstructors()方法分别会返回一个Method和一个Constructor数组。这两个类又包括一些“能把它们所代表的方法的名字,参数,返回值全部拆解开来”的方法。不过你也可以像这里所作的,只用toString()去获取一个包括这个方法的全部特征签名的String。剩下的代码就是用来抽取命令行信息,以及判断方法特征是否与你输入的字符串相匹配(用indexOf()),并且把匹配的方法列出来的。
总结:
RTTI能让你用一个匿名的基类reference来获取对象的确切类型的信息。在不懂多台方法调用的时候,这么作是理所当然的,因此新手们会自然而然的想到它,于是就用错了地方,对很多从面向过程的编程语言转过来的人来说,刚开始的时候,它们还不习惯扔掉switch语句。于是当他们用RTTI来编程的时候,就会错过多态性所带来的编程和代码维护方面的好处。Java的本义是让你在程序里面全程使用多态性,知识在万不得已的情况下才使用RTTI。
但是要想正确地使用多台方法调用,你就必须要能控制基类的定义,因为当你扩展程序的时候,可能会发现基类里面没有你想要的方法。如果这个基类是来自类库的,或是由别人控制的,那么RTTI就成解决方案了:你可以继承一个新的类,然后加上你自己的方法。在程序的其他地方,你可以检测出这个类型,调用那些特殊的方法。这样做不会破坏多态性,也不影响程序的扩展性,因为加一个新的类型不会要你去到处修改switch语句。但是,如果是在程序的主体部分加入要使用新特性的嗲马的话,你就必须使用RTTI来检查对象的确切类型了。
RTTI还会被用来解决效率问题。假设你写了一个很好的多台程序,但是运行的时候发现,有个对象反映奇慢。于是,你就可以用RTTI把则个对象捡出来,然后专门针对它的问题写代码以提高程序的运行效率,不过编程的时候切忌去过早有话代码。这是一个很有诱惑的陷阱。最好还是先让程序跑起来,然后再判断一下它跑得是不是够快了。只有觉得它还不够快,你才应该去着手解决效率问题--用profiler。
2005年03月19日 12:55 PM