Java是如何让我们在运行时识别对象和类的信息的。 主要有两种方式:
一种是“传统的"RTII, 它假定我们在编译时已经知道了所有的类型,另一种是 “反射“ 机制,它允许我们在运行时发现和使用类的信息。
1.为什么需要RTTI
RTII(Runtime type information)名字的含义:在运行时,识别一个对象的类型。
当把Shape对象放人List<Shape>的数组时会向上转型。但在向上转型为 Shape的时候也丢失了Shape对象的具体类型。对干数组而言,它们只是Shape类的对象。
当从数组中取出元素时,这种容器一实际上它将所有的举物都当作Object持有气会自动将结果转型回Shape。这是RTII最基本的使用形式,因为在Java中,所有的类型转换都是在运行时进行正确性检查的。
2.Class对象
要理解RTII在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工 作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTII,即使你正在执行的是类似转型这样的操作。
为了生成一个类的对象,运行这个程序的Java虚拟机 (JVM) 将使用被称为 “类加载器” 的子系统。
所有的类都是在对其第一次使用时,动态加载到JVM中的。
对forName()的调用是为了它产生的 “副作用”:如果类Gum还没有被加载就加载它。在加载的过程中, Gum的static子旬被执行。
使用newInstance()来创建的类,必须带有默认的构造器。
2.1 类字面常量
Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。
FancyToy.class;
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置千t可语句块中)。 并且它根除了对forName()方法的调用,所以也更高效。
类字面常量不仅可以应用千普通的类,也可以应用千接口、数组以及基本数据类型。另外, 对千基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象。
当使用 ".class" 来创建对Class对象的引用时, 不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载, 这是由类加载器执行的。 该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的), 并从这些字节码中创建一个Class对象。
- 链接。 在链接阶段将验证类中的字节码, 为静态域分配存储空间, 并且如果必需的话,将解析这个类创建的对其他类的所有引用。
- 初始化。 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行。
- 初始化。 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
但是,为了产生Class引用,Ciass.forName()立即就进行了初始化。
2.2 泛化的Class引用
Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用干这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型, 而该对象便是Class类的一个对象。
普通的类引用不会产生警告信息,你可以看到,尽管泛型类引用只能赋值为指向其声明的 类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象。通过使用泛型语法,可以让编译器强制执行额外的类型检查。
为了在使用泛化的Class引用时放松限制,我使用了通配符,它是Java泛型的一部分。通配符就是"?"'表示“任何事物”。
Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。
为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。
向 Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误?稍后立即就会发现这一点·. 在使用普通Class引用,你不会误入歧途,但是如果你确实犯了错误, 那么直到运行时你才会发现它,而这显得很不方便。
2.3 新的转型语法
cast()方法接受参数对象,并将其转型为Class引用的类型。
新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一种罕见的情况。
3.类型转换前先做检查
迄今为止,我们已知的RTTI形式包括:
- 1)传统的类型转换,如"(Shape)",由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。
- 2)代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
在编译期。编译器只能知道它是Shapre,如果不使用显式的类型转换,编译器就不允许你执行向下转型赋值,以告知编译器你拥有额外的信息,这些信息使你知道该类型是某种特定类型(编译器将检查向下转型是否合理)。
- 3)RTII在Java中还有第三种形式,就是关键字instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
3.1 使用类字面常量
3.2 动态的instanceof
Class.islnstance方法提供了一种动态地测试对象的途径。
islnstance()方法使我们不再需要instanceof。
3.3 递归计数
4.注册工厂
使用工厂方法设计模式, 将对象的创建工作交给类自己去完成。 工厂方法可以被多态地调用, 从而为你创建恰当类型的对象。
5.instanceof与Class的等价性
instanceof保持了类型的概念,它指的是 “你是这个类吗,或者你是这个类的派生类吗? ”
而如果用==比较实际的Class对象,就没有考虑继承——它或者是这个确切的类型,或者不是。
6.反射:运行时的类信息
如果不知道某个对象的确切类型,RTII可以告诉你。但是有一个限制:这个类型在编译时必须已知, 这样才能使用RTII识别它,并利用这些信息做一些有用的事。 换句话说,在编译时,编译器必须知道所有要通过RTII来处理的类。
初看起来这似乎不是个限制,但是假设你获取了一个指向某个并不在你的程序空间中的对象的引用,事实上,在编译时你的程序根本没法获知这个对象所属的类。
反射提供了一种机制一一用来检查可用的方法,并返回方法名。
人们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI),它允许一个JaVa程序将对象分布到多台机器上。
Class类与java.Iang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行 时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get()和set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
重要的是,要认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。所以RTTI和反射之间真正的区别只在于, 对RTTI来说,编译器在编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查class文件。
6.1 类方法提取器
反射在Java中是用来支持其他特性的,例如对象序列化和JavaBean。
7.动态代理
代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替 “实际”对象的对象。这些操作通常涉及与“实际”对象的通信,因此代理通常充当着中间人的角色。
设计模式的关键就是封装修改一因此你需要修改事务以证明这种模式的正确性。
Java的动态代理比代理的思想更向前迈进了一步, 因为它可以动态地创建代理并动态地处理对所代理方法的调用。
8.空对象
有时引人空对象的思想将会很有用,它可以接受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在任何“真实”对象的值。通过这种方式,你可以假设所有的对象都是有效的,而不必浪费编程精力去检查null。
但是实际上,到处使用空对象并没有任何意义一有时检查null就可以了,有时你可以合理地假设你跟本不会遇到null, 有时甚至通过NulIPointerException来探测异常也可以接受的。空对象最有用之处在于它更靠近数据,因为对象表示的是问题空间内的实体。
8.1 模拟对象与桩
空对象的逻辑变体是模拟对象和桩。与空对象一样,它们都表示在最终的程序中所使用的“实际”对象。但是,模拟对象和桩都只是假扮可以传递实际信息的存活对象,而不是像空对象那样可以成为null的一种更加智能化的替代物。
模拟对象和桩之间的差异在于程度不同。模拟对象往往是轻量级和自测试的,通常很多模 拟对象被创建出来是为了处理各种不同的测试情况。桩只是返回桩数据,它通常是重筵级的, 并且经常在测试之间被复用。桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一 种复杂对象,它要做很多事。然而对于模拟对象,如果你需要做很多事情,通常会创建大量小而简单的模拟对象。
9.接口和类型信息
通过使用反射,仍旧可以到达并调用所有方法,甚至是private方法!如果知道方法名,你就可以在其Method对象上调用setAccessible(true)。
10.总结
面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。
可继承一个新类,然后添加你需要的方法。在代码的其他地方,可以检查你自己特定的类型,并调用你自己的方法。如果在程序主体中添加需要的新特性的代码,就必须使用RTTI来检查你的特定的类型。
一致的错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检查的代码是值得的,只要你确实能够这么做。但是我相信动态代码是将Java与其他诸如C++这样的语言区分开的重要工具之一。