Thinking in Java 第14章 类型信息(type information)
发布日期:2021-06-29 02:39:33 浏览次数:2 分类:技术文章

本文共 26038 字,大约阅读时间需要 86 分钟。

运行时类型信息(Runtime type information (RTTI))使得你可以在程序运行时发现和使用类型信息。

它使你从只能在编译期执行面向类型的操作的禁锢中解脱出来,并且可以使用某些非常强大的程序。

Java主要有2种方式可以在运行时识别对象和类的信息:一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息;另一种是反射机制,它允许我们在运行时发现和使用类的信息。

14.1 为什么需要RTTI(The need for RTTI)

让我们来思考已经很熟悉了的一个使用了多态的类层次结构的例子。最一般化的类型是基类Shape,而派生出的具体类有 Circle,Square和 Triangle。

在这里插入图片描述

这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程基本的目的是:你的代码只操纵对基类(这里是Shape)的引用。这样,如果你要添加一个新类(比如从 Shape派生 Rhomboid)来扩展程序,就不会影响到原来的代码。在这个例子的 Shape接口中动态绑定了 draw()方法,目的就是让客户端程序员使用一般化的 Shape的引用来调用 draw()。draw()在所有派生类里都会被重载,并且由于它是被动态绑定的,所以即使是通过通用的 Shape引用来调用,也能产生正确行为。这就是多态( polymorphism)。

因此,我们通常会创建一个特定的对象( Circle,Square,或者 Triangle),把它向上转型成 Shape(忽略对象的特定类型),并在后面的程序中使用匿名(译注:即不知道具体类型)的 Shape引用。

abstract class Shape {
void draw() {
System.out.println(this + ".draw()"); } @Override abstract public String toString();}class Circle extends Shape {
@Override public String toString() {
return "Circle"; }}class Square extends Shape {
@Override public String toString() {
return "Square"; }}class Triangle extends Shape {
@Override public String toString() {
return "Triangle"; }}public class Shapes {
public static void main(String[] args) {
List
shapes = Arrays.asList(new Circle(), new Triangle(), new Square()); for (Shape shape : shapes) {
shape.draw(); } }}

运行结果:

Circle.draw()Triangle.draw()Square.draw()

基类中包含 draw() 方法,它通过传递 this 参数给 System.out.println, 间接地使用 toStirng() 打印标识类符(注意,toString() 被声明为 abstract, 以此强制继承者复写该方法,并可以防止对无格式的 shape 的实例化)。

如果某个对象出现在字符串表达式中(涉及 “+” 和字符串对象的表达式),toString() 方法会被自动调用,以生成表示该对象的 String。每个派生类都要覆盖(从Object继承来的)toString() 方法,这样 draw() 在不同情况下就打印出不同的消息。——这也就是多态。

在这个例子中,当把 Shape 对象放入List<Shape>的数组时会向上转型。但是向上转型为 Shape 的时候也丢失了 Shape 对象的具体类型。对于数组而言,它们只是 Shape 类的对象。

当从数组中取出元素时,这种容器——实际上它将所有的事物都当作 Object 持有 —— 会自动将结果转型回 Shape。这是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换都是在运行时进行正确性检查。这也是RTTI(Run-Time Type Interfaec)的含义:在运行时,识别一个对象的类型。

在这个例子中,RTTI 类型转化并不彻底:Object被转型为Shape,而不是转型为 Circle、Square 、 Triangle。这是因为目前我们只知道这个List<Shape>保存的是 Shape。在编译时,将由容器和 Java 的泛型系统来强制确保这一点;而在运行时,由类型转化操作来确定这一点。

接下来就是多态机制的事情了,Shape 对象实际执行了什么的代码,是由引用所指向的具体对象 Circle 、Square 、Triangle 而决定的。通常,也是这样要求的,你希望大部分代码尽可能地少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(这个例子中是Shape)。这样的代码会更容易写容易读容易维护。所以,“多态”是面向对象编程的基本目标。

但是,假如你碰到了一个特殊的编程问题,如果你能够知道某个引用得确切类型,就可以使用最简单的方式去解决它,那么此时你又该怎么办呢?例如,假设我们允许用户将某一具体类型的几何形状全都变成紫色,以突出显示它们。通过这种方法,用户就能找出屏幕上所有被突出显示的三角形。或者,你的方法可能被用来旋转列表中的所有图形,但你想跳过圆形,因为对圆形作旋转没有意义。使用 RTTI,你可以查询某个 Shape引用所指向的对象的确切类型,然后选择或者剔除特例

14.2 Class 对象(The Class object)

要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class 对象就是来创建类的所有的 “常规”对象的。Java 使用 Class 对象来执行其 RTTI,即使你正在执行的是类似类型转换这样的操作。Class 类还拥有大量的使用 RTTI 的其他方式。

类是程序的一部分,每个类都有一个 Class 对象。换言之,每当编写并编译了一个新类,就会产生一个 Class 对象(更恰当地说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,运行这个程序的 Java 虚拟机(JVM)将使用被称为 “类加载器” 的子系统。

类加载器子系统实际上可以包含一条类加载器链,但是只有一个是原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是所谓的可信类,包括 Java API 类,通常是从本地盘加载的。

所有类都是在对其第一次使用时,动态加载到 JVM 中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用 static 关键字。因此,使用 new 操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,Java 程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。这一点与许多传统语言都不同。

类加载器首先查看这个类的 Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找 .class文件。在这个类的字节码被加载时,它们会接受验证,以确保没有被破坏。

一旦某个类的 Class对象被载入内存,它就被用来创建这个类的所有对象。下面的释放程序可以证明这一点:

package typeinfo;//: typeinfo/SweetShop.java// Examination of the way the class loader works.import static net.mindview.util.Print.*;class Candy {
static {
print("Loading Candy"); }}class Gum {
static {
print("Loading Gum"); }}class Cookie {
static {
print("Loading Cookie"); }}public class SweetShop {
public static void main(String[] args) {
print("inside main"); new Candy(); print("After creating Candy"); try {
Class.forName("Gum"); } catch(ClassNotFoundException e) {
print("Couldn't find Gum"); } print("After Class.forName(\"Gum\")"); new Cookie(); print("After creating Cookie"); }} /* Output:inside mainLoading CandyAfter creating CandyLoading GumAfter Class.forName("Gum")Loading CookieAfter creating Cookie*///:~

这里的每个类 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。这里我们只需简单报告问题,但在更严密的程序里,可能要在异常处理程序中解决这个问题。

无论何时,只要你想在运行时使用类型信息(type information),就必须首先获得对恰当的Class对象的引用。Class.forName()就是实现此功能的便捷途径,因为你不需要为了获得Class引用而持有该类型的对象。但是,如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分, 它将返回表示该对象的实际类型的Class引用。Class包含很多有用的方法,下面是其中的一部分:

//: typeinfo/toys/ToyTest.java// Testing class Class.package typeinfo.toys;import static net.mindview.util.Print.*;interface HasBatteries {
}interface Waterproof {
}interface Shoots {
}class Toy {
// Comment out the following default constructor // to see NoSuchMethodError from (*1*) Toy() {
} Toy(int i) {
}}class FancyToy extends Toyimplements HasBatteries, Waterproof, Shoots {
FancyToy() {
super(1); }}public class ToyTest {
static void printInfo(Class cc) {
print("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]"); print("Simple name: " + cc.getSimpleName()); print("Canonical name : " + cc.getCanonicalName()); } public static void main(String[] args) {
Class c = null; try {
c = Class.forName("typeinfo.toys.FancyToy"); } catch(ClassNotFoundException e) {
print("Can't find FancyToy"); System.exit(1); } printInfo(c); for(Class face : c.getInterfaces()) printInfo(face); Class up = c.getSuperclass(); Object obj = null; try {
// Requires default constructor: obj = up.newInstance(); } catch(InstantiationException e) {
print("Cannot instantiate"); System.exit(1); } catch(IllegalAccessException e) {
print("Cannot access"); System.exit(1); } printInfo(obj.getClass()); }} /* Output:Class name: typeinfo.toys.FancyToy is interface? [false]Simple name: FancyToyCanonical name : typeinfo.toys.FancyToyClass name: typeinfo.toys.HasBatteries is interface? [true]Simple name: HasBatteriesCanonical name : typeinfo.toys.HasBatteriesClass name: typeinfo.toys.Waterproof is interface? [true]Simple name: WaterproofCanonical name : typeinfo.toys.WaterproofClass name: typeinfo.toys.Shoots is interface? [true]Simple name: ShootsCanonical name : typeinfo.toys.ShootsClass name: typeinfo.toys.Toy is interface? [false]Simple name: ToyCanonical name : typeinfo.toys.Toy*///:~

在传递给forName的字符串中,你必须使用全限定名。在main中Class.getInterfaces()方法返回的是Class对象,它们表示在感兴趣的Class对象中所包含的接口。getSuperClass也一样。

Class的newInstance()方法是实现“虚拟构造器”的一种途径,虚拟构造器允许你声明:“我不知道你的确切类型,但是无论如何要正确地创建你自己。”在前面的示例中,up仅仅只是一个Class引用,在编译期不具备任何更进一步的类型信息。当你创建新实例时,会得到Object引用,但是这个引用指向的是Toy对象。当然,在你可以发送Object能够接受的消息之外的任何消息之前,你必须更多地了解它,并执行某种转型。另外,使用newInstance()来创建的类,必须带有默认的构造器。在本章稍后部分,你将会看到如何通过使用Java的反射API,用任意的构造器来动态地创建类的对象。

14.2.1 类字面常量(Class literals)

Java 还提供了另一种方法来生成对 Class 对象的引用,即使用类字面常量。对上述程序来说,就像下面这样:

FancyToy.Class

这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且它根除了对forName()方法的调用,所以也更加高效。

类字面量不仅可以应用于普通的类,也可以应用于接口、数组及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,执行对应的基本数据类型的Class对象,如下所示:

在这里插入图片描述
我建议使用“.class”的形式,以保持与普通类的一致性。
注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载,这是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建一个Class对象。
  2. 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行:

package typeinfo;//: typeinfo/ClassInitialization.javaimport java.util.*;class Initable {
static final int staticFinal = 47; static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); static {
System.out.println("Initializing Initable"); }}class Initable2 {
static int staticNonFinal = 147; static {
System.out.println("Initializing Initable2"); }}class Initable3 {
static int staticNonFinal = 74; static {
System.out.println("Initializing Initable3"); }}public class ClassInitialization {
public static Random rand = new Random(47); public static void main(String[] args) throws Exception {
Class initable = Initable.class; System.out.println("After creating Initable ref"); // Does not trigger initialization: System.out.println(Initable.staticFinal); // Does trigger initialization: System.out.println(Initable.staticFinal2); // Does trigger initialization: System.out.println(Initable2.staticNonFinal); Class initable3 = Class.forName("Initable3"); System.out.println("After creating Initable3 ref"); System.out.println(Initable3.staticNonFinal); }} /* Output:After creating Initable ref47Initializing Initable258Initializing Initable2147Initializing Initable3After creating Initable3 ref74*///:~

初始化有效地实现了尽可能的“惰性”。从对initable引用的创建中可以看到,仅使用.class语法来获得对类的引用不会引发初始化。但是,为了产生Class引用,Class.forName()立即就进行了初始化,就像在对initable3引用的创建中所看到的。

如果一个static final值是“编译期常量”,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,例如,对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是一个编译期常量。

如果一个static域不是final的,那么对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配空间)和初始化(初始化该存储空间),就像在对Initable2.staticNonFinal的访问中所看到的那样。

14.2.2 泛化的 Class 引用(Generic class references)

Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。

但是,java SE5的设计者们看准机会,将它的类型变得更具体了一些,而这是通过允许你对Class引用所指向的Class对象的类型进行限定而实现的,这里用到了泛型语法。在下面的实例中,两种语法都是正确的:

package typeinfo;//: typeinfo/GenericClassReferences.javapublic class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class; Class
genericIntClass = int.class; genericIntClass = Integer.class; // Same thing intClass = double.class; // genericIntClass = double.class; // Illegal }} ///:~

普通的类引用(ordinary class reference)不会产生警告信息,你可以看到,尽管泛型类引用(generic class reference)只能赋值为指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象。通过使用泛型语法,可以让编译器强制执行额外的类型检查。

如果你希望稍微放松一些这种限制,应该怎么办呢?乍一看,好像你应该能够执行类似下面这样的操作:

Class
genericNumberClass = int.class;

这看起来似乎是起作用的,因为Integer继承自Number。但是它无法工作,因为Integer Class对象不是Number Class对象的子类。为了在使用泛化的Class引用时放松限制,我使用了通配符,它是java泛型的一部分。通配符就是“?”,表示“任何事物”。因此,我们可以在上例的普通Class引用中添加通配符,并产生相同的结果:

package typeinfo;//: typeinfo/WildcardClassReferences.javapublic class WildcardClassReferences {
public static void main(String[] args) {
Class
intClass = int.class; intClass = double.class; }} ///:~

在java SE5中,Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。

为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。因此,与仅仅声明Class<Number>不同,现在做如下声明:

package typeinfo;//: typeinfo/BoundedClassReferences.javapublic class BoundedClassReferences {
public static void main(String[] args) {
Class
bounded = int.class; bounded = double.class; bounded = Number.class; // Or anything else derived from Number. }} ///:~

向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后立即就会发现这一点。在使用普通Class引用,你不会误入歧途,但是如果你确实犯了错误,那么直到运行时你才会发现它,而这显得很不方便。

下面的示例使用了泛型类语法,它存储了一个类引用,稍后又产生了一个List,填充这个List的对象是使用newInstance()方法,通过该引用生成的:

package typeinfo;//: typeinfo/FilledList.javaimport java.util.*;class CountedInteger {
private static long counter; private final long id = counter++; public String toString() {
return Long.toString(id); }}public class FilledList
{
private Class
type; public FilledList(Class
type) {
this.type = type; } public List
create(int nElements) {
List
result = new ArrayList
(); try { for(int i = 0; i < nElements; i++) result.add(type.newInstance()); } catch(Exception e) { throw new RuntimeException(e); } return result; } public static void main(String[] args) { FilledList
fl = new FilledList
(CountedInteger.class); System.out.println(fl.create(15)); }} /* Output:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]*///:~

注意,这个类必须假设与它一同工作的任何类型都具有一个默认的构造器(无参构造器),并且如果不符合该条件,你将得到一个异常。编译器对该程序不会产生任何警告信息。

当你将泛型语法用于Class对象时,会发生一件很有趣的事情:newInstance()将返回该对象的确切类型,而不仅仅只是在ToyTest.java中看到的基本的Object。这在某种程度上有些限制:

//: typeinfo/toys/GenericToyTest.java// Testing class Class.package typeinfo.toys;public class GenericToyTest {
public static void main(String[] args) throws Exception {
Class
ftClass = FancyToy.class; // Produces exact type: FancyToy fancyToy = ftClass.newInstance(); Class
up = ftClass.getSuperclass(); // This won't compile: // Class
up2 = ftClass.getSuperclass(); // Only produces Object: Object obj = up.newInstance(); }} ///:~

如果你手头的是超类,那编译器将只允许你声明超类引用是“某个类,它是FancyToy超类”,就像在表达式Class<? Super FancyToy>中所看到的,而不会接受Class<Toy>这样的声明。这看上去显得有些怪,因为getSuperClass()方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了——在本例中就是Toy.class,而不仅仅只是“某个类,它是FancyToy的超类”。不管怎样,正是由于这种含糊性,up.newInstance()的返回值不是精确类型,而只是Object。

14.2.3 新的转型语法(New cast syntax)

java SE5还添加了用于Class引用的转型语法,即cast()方法:

package typeinfo;//: typeinfo/ClassCasts.javaclass Building {
}class House extends Building {
}public class ClassCasts {
public static void main(String[] args) {
Building b = new House(); Class
houseType = House.class; House h = houseType.cast(b); h = (House)b; // ... or just do this. }} ///:~

cast()方法接受参数对象,并将其转型为Class引用的类型。当然,如果你观察上面的代码,则会发现,与实现了相同功能的main()中最后一行相比,这种转型好像做了很多额外的工作。新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一种罕见的情况——我发现整个Java SE5类库中,只有一处使用了cast()(在com.sun.mirror.util,DeclarationFilter中)。

在java SE5中另一个没有任何用处的新特性就是Class.asSubclass(),该方法允许你将一个类对象转型为更加具体的类型。

14.3 类型转换前先做检查(Checking before a cast)

迄今为止,我们已知的RTTI形式包括:

  1. 经典的类型转换,如"(Shape)",由 RTTI确保类型转换的正确性,如果你执行了一个错误的类型转换,就会抛出一个ClassCastException异常。
  2. 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。

在 C++中,经典的类型转换 "(Shape)"并不使用 RTTI。它只是简单地告诉编译器将这个对象作为新的类型对待。而 Java要执行类型检查,这通常被称为“类型安全的向下转型(type-safe downcast)”。之所以叫“向下转型”,是由于类层次结构图从来就是这么排列的。如果将Circle类型转换为Shape被称作向上转型,那么将Shape转型为Circle,就被称为向下转型。但是,由于知道Circle肯定是一个Shape,所以编译器允许自由地做向上转型的赋值操作,而不需要任何显式的转型操作。编译器无法知道对于给定的Shape到底是什么Shape——它可能就是Shape,或者是Shape的子类型,例如Circle、Square、Traingle或某种其他的类型。在编译期。编译器只能知道它是Shape。因此,如果不使用显式的类型转换,编译器就不允许你执行向下转型赋值,以告知编译器你拥有额外的信息,这些信息使你知道该类型是某种特定类型(编译器将检查向下转型是否合理,因此它不允许向下转型到实际上不是待转型类的子类的类型上)。

RTTI在java中还有第三种形式,就是关键字instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。可以用提问的方式使用它,就像这样:

if(x instanceof Dog)    ((Dog)x).bark();

在将x转型成一个Dog前,上面的if语句会检查对象x是否从属于Dog类。进行向下转型前,如果没有其他信息可以告诉你这个对像是什么类型,那么使用instanceof是非常重要的,否则会得到一个ClassCastException异常。

一般,可能想要查找某种类型(比如要找三角形,并填充成紫色),这时可以轻松地使用instanceof来计数所有对象。例如,假设你有一个类的继承体系,描述了Pet(以及他们的主人,这是在后面的示例中出现的一个非常方便的特性)。在这个继承体系中的每个Individual都有一个id和一个可选的名字。 尽管下面的类都继承自Individual,但是Individual类复杂性较高,因此其代码将放到第17章中进行说明与解释。正如你可以看到的 ,此处并不需要去了解Individual的代码——你只需了解你可以创建其具名或不具名的对象,并且每个Individual都有一个id()方法,可以返回其唯一的标识符(通过对每个对象计数而创建的)。还有一个toString()方法,如果你没有为Individual提供名字,toString()方法只产生类型名字。

14.3.1 使用类字面常量(Using class literals)

14.3.2 动态的instanceof(A dynamic instanceof)

14.3.3 递归计数(Counting recursively)

14.4 注册工厂(Registered factories)

14.5 instanceof与Class的等价性(instanceof vs. Class equivalence)

14.6 反射:运行时的类信息(Reflection: runtime class information)

如果不知道某个对象的确切类型,RTTI可以告诉你。但有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编译器必须知道所有要通过RTTI来处理的类。

初看起来似乎这并不是一个限制,但是假设你获取了一个指向某个并不在你程序空间的对象的引用,事实上,在编译时你的程序根本没法获知这个对象所属的类。例如,假设你从磁盘文件,或者网络连接中获取了一串字节,并且你被告知这些字节代表了一个类。既然这个类在编译器为你的程序生成代码之后很久才会出现,那么怎样才能使用这样的类呢?

在传统的编程环境中不太可能出现这种情况。但当我们置身于更大规模的编程世界中,在许多重要情况下就会发生上面的事情。首先就是“基于构件的编程( component-based programming)”,在此种编程方式中,将使用某种基于快速应用开发( RAD, Rapid Application Development)的应用构建工具,即

集成开发环境(IDE),来构建项目。这是一种可视化编程方法,可以通过将代表不同组件的图标拖曳到表单中来创建程序。然后在编程时通过设置构件的属性值来配置它们。这种设计时的配置,要求构件都是可实例化的,并且要暴露其部分信息,以允许程序员读取和设置构件的属性。此外,处理图形化用户界面(GUI)事件的构件还必须暴露相关方法的信息,以便IDE能够帮助程序员覆盖这些处理事件的方法。反射提供了一种机制——用来检查可用的方法,并返回方法名。 Java通过 JavaBean(第22章将详细介绍)提供了基于构件的编程架构。

人们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI,Remote Method Invocation),它允许一个Java程序将对象分布到多态机器上。需要这种分布能力是有许多原因的。例如,你可能正在进行一项需进行大量计算的任务,为了提高运算速度,想将计算划分为许多小的计算单元,分布到空闲的机器上运行。又比如,你可能希望将处理特定类型任务的代码(例如多层的 C/S(客户/服务器)架构中的“业务规则”),置于特定的机器上,于是这台机器就成为了描述这些动作的公共场所,可以很容易地通过改动它就达到影响系统中所有人的效果。(这是一种有趣的开发方式,因为机器的存在仅仅是为了方便软件的改动!)。同时,分布式计算也支持执行特殊任务的专有硬件,例如矩阵转置,而这对于通用型程序就显得不太合适或者太昂贵了。

Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了 Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Construcotr创建新的对象,用get()和set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethod()和getConstructors()等很便利的方法,以返回表示字段,方法以及构造器的对象的数组(在 JDK文档中,可找到与 Class类相关的更多的资料)。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

重要的是,要认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只是简单的检查这个对象,看它属于哪个特定的类(就象 RTTI那样)。在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件

14.6.1 类方法提取器

通常你不需要直接使用反射工具,但是它们在你需要创建更加动态的代码时会很有用。反射在java中是用来支持其他特性的,例如对象序列化和JavaBean。但是,如果能动态地提取某个类的信息有的时候还是很有用的。请考虑类方法提取器。浏览实现了类定义的源代码或是其JDK文档,只能找到在这个类定义中被定义或被覆盖的方法。但对你来说,可能有数十个更有用的方法都是继承自基类的。要找出这些方法可能会很乏味且费时。幸运的是,反射机制提供了一种方法,使我们能够编写可以自动展示完整接口的简单工具。下面就是其工作方式:

package typeinfo;//: typeinfo/ShowMethods.java// Using reflection to show all the methods of a class,// even if the methods are defined in the base class.// {Args: ShowMethods}import java.lang.reflect.*;import java.util.regex.*;import static net.mindview.util.Print.*;public class ShowMethods {
private static String usage = "usage:\n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or:\n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; private static Pattern p = Pattern.compile("\\w+\\."); public static void main(String[] args) {
if(args.length < 1) {
print(usage); System.exit(0); } int lines = 0; try {
Class
c = Class.forName(args[0]); Method[] methods = c.getMethods(); Constructor[] ctors = c.getConstructors(); if(args.length == 1) {
for(Method method : methods) print( p.matcher(method.toString()).replaceAll("")); for(Constructor ctor : ctors) print(p.matcher(ctor.toString()).replaceAll("")); lines = methods.length + ctors.length; } else {
for(Method method : methods) if(method.toString().indexOf(args[1]) != -1) {
print( p.matcher(method.toString()).replaceAll("")); lines++; } for(Constructor ctor : ctors) if(ctor.toString().indexOf(args[1]) != -1) {
print(p.matcher( ctor.toString()).replaceAll("")); lines++; } } } catch(ClassNotFoundException e) {
print("No such class: " + e); } }} /* Output:public static void main(String[])public native int hashCode()public final native Class getClass()public final void wait(long,int) throws InterruptedExceptionpublic final void wait() throws InterruptedExceptionpublic final native void wait(long) throws InterruptedExceptionpublic boolean equals(Object)public String toString()public final native void notify()public final native void notifyAll()public ShowMethods()*///:~

Class的getMethods()和getConstructors()方法分别返回Method对象的数组和Constructor对象的数组。这两个类都提供了深层方法,用以解析其对象所代表的方法,并获取其名字、输入参数以及返回值。但也可以像这里一样,只使用toString()生成一个含有完整的方法特征签名的字符串。代码其他部分用于提取命令行信息,判断某个特定的特征签名是否与我们的目标字符串相符(使用indexOf()),并使用正则表达式去掉了命名修饰词。

Class.forName()生成的结果在编译时是不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的。如果研究一下JDK文档中关于反射的部分,就会看到,反射机制提供了足够的支持,使得能够创建一个在编译时完全未知的对象,并调用此对象的方法。虽然开始的时候可能认为永远也不需要用到这些功能,但是反射机制的价值是很惊人的。

上面的输出是从下面的命令行产生的:

java ShowMethods ShowMethods

你可以看到,输出中包含了一个public的默认构造器,即便能在代码中看到根本没有定义任何构造器。所看到的这个包含在列表中的构造器是编译器自动合成的。如果将ShowMethods作为一个非public的类(也就是拥有包访问权限),输出中就不会再显示出这个自动合成的默认构造器了。该自动合成的默认构造器会自动被赋予与类一样的访问权限。

还有一个有趣的例子是,用一个额外的 char、int或String等参数来调用 java ShowMethods java.lang.String。

在编程时,特别是如果不记得一个类是否有某个方法,或者不知道一个类究竟能做些什么,例如Color对象,而又不想通过索引或类的层次结构去查找 JDK文档,这时这个工具确实能节省很多时间。

14.7 动态代理(Dynamic proxies)

代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替 “实际” 对象的对象。这些操作通常涉及与“实际”对象的通信,因此代理通常充当着中间人的角色。下面是一个用来展示代理结构的简单示例:

package typeinfo;//: typeinfo/SimpleProxyDemo.javaimport static net.mindview.util.Print.*;interface Interface {
void doSomething(); void somethingElse(String arg);}class RealObject implements Interface {
public void doSomething() {
print("doSomething"); } public void somethingElse(String arg) {
print("somethingElse " + arg); }} class SimpleProxy implements Interface {
private Interface proxied; public SimpleProxy(Interface proxied) {
this.proxied = proxied; } public void doSomething() {
print("SimpleProxy doSomething"); proxied.doSomething(); } public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg); proxied.somethingElse(arg); }} class SimpleProxyDemo {
public static void consumer(Interface iface) {
iface.doSomething(); iface.somethingElse("bonobo"); } public static void main(String[] args) {
consumer(new RealObject()); consumer(new SimpleProxy(new RealObject())); }} /* Output:doSomethingsomethingElse bonoboSimpleProxy doSomethingdoSomethingSimpleProxy somethingElse bonobosomethingElse bonobo*///:~

因为consumer()接受的是Interface,所以他无法知道获得的到底是RealObject还是SimpleProxy,因为这二者都实现了Interface。但是SimpleProxy已经被插入到了客户端和RealObject之间,因此他会执行操作,然后调用RealObject上相同的方法。

在任何时刻,只要你想要将额外的操作从“实际”对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用(设计模式的关键就是封装修改——因此你需要修改事物以证明这种模式的正确性)。例如,如果你希望跟踪RealObject中的方法的调用,或者希望度量这些调用的开销,那么你应该怎么做呢?这些代码肯定是你不希望将其合并到应用中的代码,因此代理使得你可以很容易地添加或移除它们

Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器( invocation handler)上,它的工作是揭示调用的类型并确定相应的对策(which has the job of discovering what the call is and deciding what to do about it)。下面是用动态代理重写的SimpleProxyDemo.java:

package typeinfo;//: typeinfo/SimpleDynamicProxy.javaimport java.lang.reflect.*;class DynamicProxyHandler implements InvocationHandler {
private Object proxied; public DynamicProxyHandler(Object proxied) {
this.proxied = proxied; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("**** proxy: " + proxy.getClass() + ", method: " + method + ", args: " + args); if(args != null) for(Object arg : args) System.out.println(" " + arg); return method.invoke(proxied, args); }} class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething(); iface.somethingElse("bonobo"); } public static void main(String[] args) {
RealObject real = new RealObject(); consumer(real); // Insert a proxy and call again: Interface proxy = (Interface)Proxy.newProxyInstance( Interface.class.getClassLoader(), new Class[]{
Interface.class }, new DynamicProxyHandler(real)); consumer(proxy); }} /* Output: (95% match) doSomethingsomethingElse bonobo**** proxy: class $Proxy0, method: public abstract void Interface.doSomething(), args: nulldoSomething**** proxy: class $Proxy0, method: public abstract void Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@42e816 bonobosomethingElse bonobo*///:~

通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,这个方法需要一个类加载器(你通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。动态代理可以将所有的调用重定向到调用处理器,因此通常会向调用处理器的构造器传递一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(so that it can forward requests once it performs its intermediary task)。

invoke()方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。然而,在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用。

通常,你会执行被代理的操作,然而使用Method.invoke()将请求转发给被代理对象,并传入必须的参数。这初看起来可能有些受限,就像你只能执行泛化操作一样。但是,你可以通过传递其他的参数,来过滤某些方法调用( However, you can filter for certain method calls, while passing others through ):

package typeinfo;//: typeinfo/SelectingMethods.java// Looking for particular methods in a dynamic proxy.import java.lang.reflect.*;import static net.mindview.util.Print.*;class MethodSelector implements InvocationHandler {
private Object proxied; public MethodSelector(Object proxied) {
this.proxied = proxied; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("interesting")) print("Proxy detected the interesting method"); return method.invoke(proxied, args); }} interface SomeMethods {
void boring1(); void boring2(); void interesting(String arg); void boring3();}class Implementation implements SomeMethods {
public void boring1() {
print("boring1"); } public void boring2() {
print("boring2"); } public void interesting(String arg) {
print("interesting " + arg); } public void boring3() {
print("boring3"); }} class SelectingMethods {
public static void main(String[] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance( SomeMethods.class.getClassLoader(), new Class[]{
SomeMethods.class }, new MethodSelector(new Implementation())); proxy.boring1(); proxy.boring2(); proxy.interesting("bonobo"); proxy.boring3(); }} /* Output:boring1boring2Proxy detected the interesting methodinteresting bonoboboring3*///:~

这里,我们只查看了方法名,但是你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值。

动态代理并非是你日常使用的工具,但是它可以非常好地解决某些类型的问题。你可以在《Thinking in Patterns》(查看www.MindView.net) 和Erich Gamma 等人撰写的《Design Patterns》这两本书中了解到有关代理和其他设计模式的更多知识。

14.8 空对象(Null Objects)

14.9 接口与类型信息(Interfaces and type information)

14.10 总结(Summary)

参考:

Thinking in Java 4th 第14章 类型信息

转载地址:https://blog.csdn.net/yzpbright/article/details/105326758 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:深入Java虚拟机第5章 Java虚拟机 方法区
下一篇:Core Java, Volume I 第 6 章 接口、lambda 表达式与内部类

发表评论

最新留言

关注你微信了!
[***.104.42.241]2024年04月18日 21时40分58秒