第14章 类型信息
RTTI(Run-Time Type Identification)运行阶段类型识别
运行时类型信息使得你可以在程序运行时发现和适用类型信息。
一种是“传统的”RTTI;另一种是“反射”机制
14.1 为什么需要RTTI
如果能够知道某个泛化引用的确切类型,就可以使用最简单的方式去解决它。使用RTTI,可以查询某个Shape引用所指的对象的确切类型,然后选择或者剔除特例。
14.2 Class对象
Class对象包含了与类相关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。Class类还拥有大量的使用RTTI的其他方式。
类是程序的一部分,每个类都有一个class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在了一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统。
类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是所谓的可信类,包括JavaAPI类,它们通常是从本地盘加载的。
所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当做对类的静态成员的引用。
因此,Java程序在它开始运行之前并非被完全加载,其各个部分在必需时才加载的。这一点与许多传统语言都不同。
类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码)。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一)
一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。
Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。
forName()这个方法是Class类(所有Class对象都属于这个类)的一个static成员。它是取得Class对象的引用的一种方法。 对forName()的调用有个“副作用”:如果类还没被加载就加载它。
Class.forName()找不到你要加载的类,它会抛出异常ClassNotFoundException。
如果你已经拥有了一个感兴趣的类型的对象,就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它将返回表示该对象的实际类型的Class引用。
printInfo()使用getName()来产生全限定的类名,并分别使用getSimpleName()和getCanonicalName()(在Java SE 5 中引入的)来产生不含包名的类名和全限定的类名。isInterface()方法如同其名,可以告诉你这个Class对象是否表达某个接口。
在main()中调用的Class.getInterfaces()方法返回的是Class对象,它们表示在感兴趣的Class对象中所包含的接口。
如果你有一个Class对象,还可以使用getSuperclass()方法查询其直接基类,这将返回你可以用来进一步查询的Class对象。
Class的newInstance()方法是实现“虚拟构造器”的一种途径,虚拟构造器允许你声明:“我不知道你的确切类型,但是无论如何要正确地创建你自己。” 使用newInstance()来创建的类,必须带有默认的构造器。
14.2.1 类字面常量
Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。
类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及其他基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象。例如:boolean.class等价于Boolean.TYPE
我建议用“.class”的形式,以保持与普通类的一致性。
注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并不是必须的),并从这些字节码中创建一个Class对象。
- 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用才执行。
初始化有效地实现了尽可能的“惰性”。仅使用.class语法来获得来获得对类的引用不会引发初始化。而Class.forName()立即就进行了初始化。
如果一个static final值是“编译期常量”,这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,因为它不是编译期常量。
如果一个static域不是final的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间)
14.2.2 泛化的Class引用
通过使用泛型语法,可以让编译器强制执行额外的类型检查。
Class<Number> genericNumberClass = int.class;
看起来是起作用的,因为Integer继承自Number。但是它无法工作,因为Integer Class对象不是Number Class对象的子类(这种差异看起来可能有些诡异,我们将在15章深入讨论它)
为了在使用泛化的Class引用时放松限制,我使用了通配符,它是Java泛型的一部分。通配符就是“?”,表示“任何事物”。
在Java SE5中,Class<?>优于平凡的Class,即便它们是等价的,而且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处就是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。
为了创建一个Class引用,它被限制为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。
如果你手头的是超类,那编译器将只允许你声明超类引用是“某个类,它是FancyToy超类”就像在表达式Class<? Super FancyToy>中所看的,而不会接受Class<Toy>这样的声明。这看上去显得有些怪,因为getSuperClass()方法返回的是基类(不是接口)。正是由于这种含糊性,up.newInstance()的返回值不是精确类型,而只是Object。
14.2.3 新的转型语法
Java SE5还添加了用于Class引用的转型语法,即cast()方法:
cast()方法接受参数对象,并将其转型为Class引用的类型。这种转型多做了很多额外的工作。新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一种罕见的情况——我发现整个Java SE5类库中,只有一处使用了cast()。
在Java SE5中另一个没有任何用处的新特性就是Class.asSubclass(),该方法允许你将一个类对象转型为更加具体的类型。
14.3 类型转换前先做检查
1)传统的类型转换,如“(Shape)”,由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。
2)代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
在C++中,经典的类型转换“(Shape)”并不使用RTTI。它只是简单地告诉编译器将这个对象作为新的类型对待。而Java要执行类型检查,这通常被称为“类型安全的向下转型”。
RTTI在Java中还有第三种形式,就是关键字instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
在向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用instanceof是非常重要的,否则会得到一个ClassCastException异常。
14.3.1 使用类字面常量
14.3.2 动态的instanceof
Class.isInstance方法提供了一种动态地测试对象的途径。
isInstance()方法使我们不再需要instanceof表达式
14.3.3 递归计数
Class.isAssignableFrom()
14.4 注册工厂
工厂方法设计模式,将对象的创建工作交给类自己去完成。工厂方法可以被多态地调用,从而为你创建恰当类型的对象。
14.5 instanceof与Class的等价性
使人放心的是,instanceof和isInstance()生成的结果完全一样,equals()和==也一样。instanceof保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”如果用==比较实际的Class对象,就没有考虑继承——它或者是这个确切类型,或者不是。
14.6 反射:运行时的类信息
如果不知道某个对象的确切类型,RTTI可以告诉你。但是有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它。
“基于构件的编程” 快速应用开发(RAD) 集成开发环境(IDE) 反射提供了一种机制——用来检查可用的方法,并返回方法名。Java通过JavaBeans提供了基于构件的编程架构。
远程方法架构(RMI)
Class类和java.lang.reflect类库一起反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这样你就可以使用Constructor创建新的对象,用get()和set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等很便利的方法,以返回字段、方法以及构造器的对象的数组
RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件。而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
14.6.1 类方法提取器
反射是用来支持其他特性的,例如对象序列化和JavaBean。但是如果能动态地提取某个类的信息有的时候还是很有用的。请考虑类方法提取器。
Class的getMethods()和getConstuctors()方法分别返回Method对象的数组和Constructor对象的数组。
14.7 动态代理
代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替“实际”对象的对象。这些操作通常设计与“实际”对象的通信,因此代理通常充当着中间人的角色。
Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。
通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,这个方法需要得到一个类加载器(你通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。
invoke()方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。然而,在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用。
14.8 空对象
有时引入空对象的思想会很有用,它可以接受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在任何“真实”对象的值。通过这种方式,你可以假设所有的对象都是有效的,而不必浪费编程精力去检查null(并阅读所产生的代码)。
14.8.1 模拟对象与桩
14.9 接口与类型信息
interface关键字一种重要目标就是允许程序员隔离构件、进而降低耦合性。如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保障。
一种解决方案是直接声明,如果程序员决定使用实际的类而不是接口,他们需要自己对自己负责。你也许希望应用一些更严苛的控制。
最简单的方式是对实现使用包访问权限
14.10 总结
RTTI允许通过匿名基类的引用来发现类型信息。 面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必须的时候使用RTTI。