反射

反射可以在程序运行时发现并使用对象的类型信息。

为什么需要反射

考虑一个例子,基类 Shape 有三个子类:Circle,Square和Triangle,基类 Shape 中有方法 draw()

面向对象编程的一个基本目的是:让代码只操纵对基类(这里即 Shape )的引用。这样,如果你想添加一个新类(比如从 Shape 派生出 Rhomboid)来扩展程序,就不会影响原来的代码。在这个例子中,Shape 接口中动态绑定了 draw() 方法,这样做的目的就是让客户端程序员可以使用泛化的 Shape 引用来调用 draw()draw() 方法在所有派生类里都会被重写,而且由于它是动态绑定的,所以即使通过 Shape 引用来调用它,也能产生恰当的行为,这就是多态。

因此,我们通常会创建一个具体的对象(Circle、Square 或者 Triangle),把它向上转型成 Shape (忽略对象的具体类型),并且在后面的程序中使用 Shape 引用来调用在具体对象中被重载的方法(如 draw())。

可以像下例这样,对 Shape 的层次结构进行编程:

abstract class Shape {
    void draw() {
        System.out.println(this + ".draw()");
    }
    @Override public abstract 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) {
        Stream.of(new Square(), new Circle(), new Triangle())
                .forEach(Shape::draw);
    }
}

基类里包含一个draw()方法,它通过将 this 传递给 System.out.println()。间接地使用toString()方法来显示类的标识符(注意:这里将 toStirng() 方法声明为 abstract,可以强制子类重写该方法,并防止没什么内容的 Shape 类被实例化)。如果某个对象出现在字符串拼接表达式中(该表达式需要包含 + 和 String 对象),这个对象的toString()方法就会被自动调用,来生成一个表示它自身的字符串。每个子类都要重写toString()方法(从 Object 继承而来),所以 draw() 最终(多态地)在不同情况下打印出了不同的内容。

在这个例子中,将一个 Shape 的子类对象放入 Stream<Shape> 时,会发生隐式的向上转型,在向上转型为 Shape 时,这个对象的确切类型信息就丢失了。对于流而言,它们只是 Shape 类的对象。

严格来说,Stream<Shape>实际上将所有内容都当作 Object 保存。当一个元素被取出时,它就会自动转回 Shape。这就是反射最基本的形式,在运行时检查了所有的类型转换是否正确。反射的意思就是:在运行时,确定对象的类型。

在这个例子中,反射类型转换并不彻底:Object 只是被转换成了 Shape,而没有转换为最终的 Circle, Square 或 Triangle。这是因为我们所能得到的信息就是,Stream<Shape>里保存的都是 Shape。在编译时,这是由 Stream 和 Java 泛型系统强制保证的;而在运行时,类型转换操作会确保这一点。

接下来就是多态机制的事了,Shape 对象实际执行什么样的代码,是由引用所指向的具体对象(Circle、Square 或者 Triangle)决定的。一般来说,这是合理的:我们希望自己的代码尽可能少地知道对象的确切类型信息,而只和这类对象的通用表示(Shape)打交道。这样我们的代码就更易于编写、阅读和维护,并且设计也更易实现、理解和更改。所以多态时面向对象编程的一个基本目标。

但是,有时会碰到一些特殊编程问题,在这些问题中如果你能知道某个泛化引用的确切类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道 Stream<Shape> 里边的形状具体是什么类型。通过反射,就可以查询到某个 Shape 引用所指的确切类型,从而选择并隔离特殊情况。

Class 对象

要想知道 Java 中反射的工作原理,就必须先了解类型信息在运行时是如何表示的。这项工作是通过叫作 Class 对象的特殊对象来完成的,它包含了与类相关的信息。事实上,Class 对象被用来创建类的所有“常规”对象。Java 使用 Class 对象执行反射,即使是类型转换这样的操作也一样。Class 类还有许多其它使用反射的方式。

程序中每个类都有一个 Class 对象。即每次编写并编译一个新类时,都会生成一个 Class 对象(并被相应地存储在同名的.class文件中)。为了生成这个对象,Java 虚拟机(JVM)使用被称为 类加载器(class loader) 的子系统。

类加载器子系统实际上可能包含一条类加载器链,但里面只会有一个原始类加载器,它是 JVM 实现的一部分。原始类加载器通常从本地磁盘加载所谓的可信类,包括 Java API 类。通常我们不需要加载器链中的额外加载器,但对于特殊需要(例如以某种方式加载类以支持 Web 服务器应用程序,或通过网络来下载类),你可以引入额外的类加载器来实现。

类在首次使用时才会被动态加载到 JVM 中。当程序第一次引用该类的静态成员时,就会触发这个类的加载。构造器是类的一个静态方法,尽管没有明确使用 static 关键字。因此,使用 new 操作符创建类的新对象也算作对该类静态成员的引用,构造器的初次使用会导致该类的加载。

所以,Java程序在运行前并不会被完全加载,而是在必要时加载对应的部分。这与许多传统语言不同,这种动态加载能力使得 Java 可以支持许多行为,而它们在静态加载语言(如C++)中很难复制,或根本不可能复制。

类加载器首先检查是否加载了该类型的 Class 对象。如果没有,默认的类加载器会定位到具有该名称的.class文件(如果有附加类加载器,可能会在数据库中查找对应的字节码)。当该类的字节数据被加载时,它们会被验证,以确保没有被损坏,并且不包含恶意的 Java 代码(这是 Java 安全防范的一种措施)。

一旦该类型的 Class 对象被加载到内存中,它就会用于创建该类型的所有对象。下面的示范程序可以证明这点:

Candy, Gum 和 Cookie 这三个类都有一个静态代码块,该静态代码块在类第一次加载时执行。输出的信息会告诉我们这个类是什么时候加载的。在main()方法中,对象的创建被置于打印语句之间,以方便判断类加载的时间。

从输出中可以看到,Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。

代码中这一行很有趣:

所有的 Class 对象都属于 Class 类。Class 对象和其它对象一样,因此你可以获取并操作它的引用(这也是类加载器的工作)。静态的 forName() 方法可以获得 Class 对象的引用,该方法接收了一个包含所需类的文本名称的字符串,并返回了一个 Class 引用,上面示例中的返回值被忽略,我们对 forName() 的调用只是为了它的副作用;如果类 Gum 尚未加载,则加载它。在加载过程中,会执行 Gum 的静态代码块。

如果 Class.forName() 因为找不到试图加载的类而失败,它会抛出一个 ClassNotFoundException。在这里,我们只是简单地报告了问题并继续执行,但在更复杂的程序中,可能会尝试在异常处理流程中修复这个问题。

不管什么时候,只要在运行时用到类型信息,就必须首先获得相应的 Class 对象的引用。这时 Class.forName() 方法用起来就很方便了,因为不需要对应类型的对象就能获取 Class 引用。但是,如果已经有了一个你想要的类型的对象,就可以通过 getClass() 方法来获取 Class 引用,这个方法属于 Object 根类。它返回的 Class 引用表示了这个对象的实际类型。Class 类有很多方法,下面是其中一部分:

FancyToy 继承了类 Toy 并实现了接口 HasBatteries, Waterproof 和 Shoots。在 main()中,我们在 try 块中用 forName() 创建了一个 Class 引用,并将其初始化为 FancyToy 的 Class 对象。注意,传递给 forName() 的字符串参数必须是类的完全限定名称(包括包名称)。

printInfo() 方法使用 getName() 来生成完全限定的类名,使用 getSimpleName()getCanonicalName() 分别生成不带包的名称和完全限定的名称(除内部类和数组外,对大部分类产生的结果与getName()相同)。isInterface()用于判断某个 Class 对象代表的是否为一个接口。因此,通过 Class 对象可以获取想要了解的所有类型信息。

main()中调用的Class.getInterfaces()方法返回了一个 Class 对象数组,它们表示这个 Class 对象的所有接口。

还可以使用 getSuperclass() 来查询 Class 对象的直接基类。它返回一个 Class 引用,还可以对他做进一步查询,这样就可以在运行时获取一个对象的完整类层次结构。

Class 的 newInstance() 方法是实现“虚拟构造器”的一种途径,这相当于声明“我不知道你的确切类型,但无论如何你都要正确地创建自己”。在前面的例子中,up 只是一个 Class 引用,他在编译时没有更多的类型信息。当创建一个新实例时,就会得到一个 Object 引用。但该引用指向了一个 Toy 对象。你可以给它发送 Object 能接收的信息,但如果想要发送除此之外的其他信息,就必须进一步了解它,并进行某种类型转换。此外,使用Class.newInstance() 创建的类必须有一个无参构造器。在稍后部分,你将会看到如何通过 Java 的反射 API,用任意的构造器来动态地创建类的对象。

注意,此示例中的newInstance()在Java 8中正常,但在更高版本中已被弃用,Java推荐使用 Constructor.newInstance()来代替。示例中使用了@SupperWarnings("deprecation")来抑制更高版本的弃用警告。

类字面量

Java 还提供了另一种方式来生成 Class 对象的引用:类字面量(class literal)。对前面的程序而言,就像这样:

这样更简单也更安全,因为它会进行编译时检查(因此不必放在 try 块中)。另外它还消除了对forName()方法的调用,所以效率更高。

类字面量适用于常规类以及接口、数组和基本类型。此外,每个基本包装类都有一个名为 TYPE 的标准字段。TYPE 字段表示指向和基本类型对应的 Class 对象的引用,如 boolean.class 等价于 Boolean.TYPE,还有char, byte, short, int, ..., void等。

建议尽可能使用".class"的形式,因为它与常规类更一致。

注意,使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。实际上,在使用一个类之前,需要先执行以下 3 个步骤。

  1. 加载,这是由类加载器执行的。该步骤先会找到字节码(通常在类路径中的磁盘上,但也不一定),然后从这些字节码中创建一个 Class 对象。

  2. 链接,在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并在必要时解析该类对其他类的所有引用。

  3. 初始化,如果有基类,会先初始化基类,执行静态初始化器和静态初始化块。

首次引用静态方法(构造器是隐式静态的)或非常量静态字段,才进行初始化:

初始化会“尽可能懒惰”。从 initable 引用的创建过程中可以看出,仅使用 .class 语法来获取对类的引用不会导致初始化。而Class.forName()会立即初始化类以产生 Class 引用,如 initable3 的创建所示。

如果一个 static final 字段的值是“编译时常量”,如 Initable.staticFinal,那么这个值不需要初始化 Initable 类就能读取。但是把一个字段设置为 static 和 final 并不能保证这种行为,对 Initable.staticFinal2 的访问会强制执行类的初始化,因为它不是编译时常量。

如果 static 字段不是 final 的,那么访问它时,如果想要正常读取,总是需要先进行链接(为字段分配存储)和初始化(初始化该存储),正如对 Initable.staticNonFinal 的访问那样。

泛型类的引用

Class 引用指向的是一个 Class 对象,该对象可以生成类的实例,并包含了这些实例所有方法的代码。它还包含该类的静态字段和静态方法。所以一个 Class 引用表示的就是它所指向的确切类型:Class 类的一个对象。

你可以使用泛型语法来限制 Class 引用的类型。下面示例中两种语法都是正确的:

intClass 可以重新赋值为任何其它的 Class 对象,例如 double.class,而不会产生警告。泛化的类引用 genericIntClass 只能分配给其声明的类型。通过泛型语法,可以让编译器强制执行额外的类型检查。

如果想要稍微放松一下限制,如下面,乍一看好像是可以执行的:

这似乎是有道理的,因为 Integer 继承了 Number。但实际上这段代码无法运行,因为 Integer 的 Class 对象不是 Number 的 Class 对象的子类(将在下一章详细讨论)。

要想放松使用泛化的 Class 引用时的限制,应该使用通配符?,它是 Java 泛型的一部分,表示“任何事物”。在上面的例子中为普通的 Class 引用加上通配符,就可以产生相同的结果:

尽管前面的普通 Class 不会产生编译器警报,但是我们更倾向于 Class<?>,即使它们是等价的。使用 Class<?>的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。

如果想创建一个 Class 引用,并将其限制为某个类型或任意子类型,可以将通配符与 extends 关键字组合来创建一个界限(bound)。因此,与其使用 Class<Number>,不如像下面这样:

向 Class 引用添加泛型语法的一个原因是为了提供编译时的类型检查,因此如果你操作有误,稍后就会发现这点。

下面的示例使用了泛型语法,它保存了一个类引用,稍后又用 newInstance() 方法产生类的对象:

DynamicSupplier 会强制要求它使用的任何类型都有一个 public 的无参构造器,若不符合条件就会抛出异常。该例子中 ID 自动生成的无参构造器不是 public 的,因为 ID 类不是 public 的。而当 ID 是 public 时,就不需要显示定义。

对 Class 对象使用泛型语法时,newInstance() 会返回对象的确切类型,而不仅仅是如 ToyTest.java 中那样简单的 Object。但是它也会受到一些限制:

如果得到了基类,那么编译器只允许你声明这个基类引用是“FancyToy 的某个基类”,就像表达式Class<? super FancyToy>所声明那样。他不能被声明为Class<Toy>,有点奇怪,因为 getSuperclass() 返回了基类(不是接口),而编译器在编译时就知道这个基类是什么(在这里是 Toy.class),而不仅仅是“FancyToy 的某个基类”。不管怎样,因为存在这种模糊性,所以 up.getConstructor().newInstance()的返回值不是一个确切的类型,而只是一个 Object。

cast() 方法

该方法用于 Class 引用的转型语法:

cast() 方法接收参数对象并将其转换为 Class 引用的类型。如上示例所示,最后一行也可以完成相同的工作,这种方式似乎做了许多额外工作。

cast() 在无法使用普通类型转换的情况下会显得非常有用,在你编写泛型代码时,如果你保存了 Class 引用,并希望以后通过这个引用来执行转型,你就需要用到 cast()。但事实却是这种情况非常少见,我发现整个 Java 类库中,只有一处使用了 cast()(在 com.sun.mirror.util.DeclarationFilter 中)。

另一个在 Java 库中没有用到的特性是 Class.asSubclass(),它会将类对象转换为更具体的类型。

转型前检查

目前,已学习的内容如下:

  1. 传统的类型转换。如" (Shape) ",它使用反射来确保转型是正确的。如果执行了错误的转型,就会抛出一个 ClassCastException

  2. 代表对象类型的 Class 对象,通过查询 Class 对象可以获取运行时所需的信息。

在 C++ 中,传统的类型转换 " (Shape) "并不执行反射(运行时类型检查,RTTI),它只是告诉编译器将对象视为新类型。而 Java 会进行类型检查,这种类型转换一般被称作“类型安全的向下转型”。之所以称作“向下转型”,是因为类层次结构图从来就是这么排列的。将 Circle 转换为 Shape 是一次向上转型, 将 Shape 转换为 Circle 是一次向下转型。但是, 因为我们知道 Circle 肯定是一个 Shape,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显式的转型操作。当你给编译器一个 Shape 的时候,编译器并不知道它到底是什么类型的 Shape——它可能是 Shape,也可能是 Shape 的子类型,例如 Circle、Square、Triangle 或某种其他的类型。在编译期,编译器只能知道它是 Shape。因此,你需要使用显式地进行类型转换,以告知编译器你想转换的特定类型,否则编译器就不允许你执行向下转型赋值。 (编译器将会检查向下转型是否合理,因此它不允许向下转型到实际不是待转型类型的子类类型上)。

Java 还有第三种形式的反射,就是关键字 instanceof,它返回一个 boolean 值,表明一个对象是否是特定类型的实例。可以用提问的方式使用它,就像这个样子:

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

通常,即使只想寻找确切的类型(例如要找三角形,并填充为紫色),也可以使用 instanceof 来轻松识别所有的对象。假如你有一个类的继承体系,描述了 Pet(以及它们的主人,在后面一个例子中会用到这个特性)。在这个继承体系中的每个 Individual 都有一个 id 和一个可选的名字。尽管下面的类都继承自 Individual,但是 Individual 类复杂性较高,因此其代码将在容器章节中进行解释说明。正如你所看到的,此处并不需要去了解 Individual 的代码——你只需了解你可以创建其具名或不具名的对象,并且每个 Individual 都有一个 id() 方法来返回其唯一标识符,如果你没有为 Individual 提供名字,toString()方法就会生成简单的类型名称。

这里的继承体系是这样的:Person 和 Pet 继承自 Individual,然后 Pet 的子类有 Dog, Cat, Rodent。Dog 的子类有 Mutt 和 Pug。Cat 的子类有 EgyptianMau 和 Manx,而 Manx 又有子类 Cymric。Rodent 的子类有 Rat,Mouse,Hamster。

这里必须显式地为每一个子类编写无参构造器。因为我们有一个带一个参数的构造器,所以编译器不会自动地为我们加上无参构造器。

接下来,我们需要一个类,它可以随机地创建不同类型的宠物,同时,它还可以创建宠物数组和持有宠物的 List。为了使这个类更加普遍适用,我们将其定义为抽象类:

这是模板方法(Template Method)设计模式的一个例子,抽象的 type() 方法需要在 Creator 的子类里实现,以生成一个包含了 Class 对象的 List。注意,List 的泛型参数被指定为“继承了 Pet 的任意子类”,因此 newInstance() 无须类型转换即可生成一个 Pet。get()会查找 List 的索引来生成一个 Class 对象,getConstructor() 会生成一个 Constructor 对象,而 newInstance() 使用该 Constructor 来创建一个对象。

调用 newInstance() 时可能会得到四种异常。这些异常的名称本身很好地解释了它们所代表的错误内容(IllegalAccessException 表示违反了 Java 的安全机制,在该例中,如果无参构造器是 private 的,就会抛出该异常)。

Creator 类提供了几个工具方法来创建一组随机生成的 Pet:stream()方法生成一个 Stream<Pet>array()方法生成一个 Pet[]list() 方法生成一个List<Pet>

在实现 Creator 的子类时,必须提供一个 Pet 类型的 List,这样才可以调用 get() 方法来获取 Pet 对象。type()方法一般来说只需返回一个静态 List 的引用就可以了。下面是使用 forName() 实现的实例:

要生成具有实际类型的 Class 对象的列表,就必须进行强制类型转换,这会产生编译时警告。这里单独定义了 loader() 方法,然后在静态初始化块中调用了它,这是因为@SuppressWarnings("unchecked")注解不能直接用于初始化块。

若想要知道 Pet 有多少,就需要来跟踪各种不同类型的 Pet 的数量。此时采用 Map 就非常合适:键可以是 Pet 类型的名称,而值则是保存了 Pet 数量的 Integer。通过这种方式,你就可以询问:“有多少个 Hamster 对象?”我们可以使用 instanceof 来对 Pet 进行计数:

countPet()中,使用 instanceof 来对数组中的每个 Pet 进行测试和计数。

instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。在前面的例子中,你可能会觉得写出一大堆 instanceof 表达式很乏味,事实也是如此。但是,也没有办法让 instanceof 聪明起来,让它能够自动地创建一个 Class 对象的数组,然后将目标与这个数组中的对象逐一进行比较(稍后会看到一种替代方案)。其实这并不是那么大的限制,如果你在程序中写了大量的 instanceof,那就说明你的设计可能存在瑕疵。

使用类字面量

若使用类字面量重新实现 PetCreator 类的话,其结果在很多方面都会更清晰:

由于这次的 types 的创建在编译时被检查,因此不会抛出异常,不需要放在 try 块中。

在下面的 PetCounter3.java 示例中,会预先加载一个包含所有 Pet 类型(不仅仅是那些随机生成的)的 Map,因此这个 ALL_TYPES 的 List 是必要的。types 列表是 ALL_TYPES 的一部分,包含了确切的宠物类型,因此可以用来生成随机的 Pet。

PetCounter.count()接受一个 Creator 参数,如之前类似测试 PetCreator,输出与 PetCounter.java 相同。

动态的 instanceof

Class.isInstance() 方法提供了一种动态验证对象类型的方法。因此,所有这些繁琐的 instanceof 语句都可以从 PetCount.java 中删除:

为了对所有不同类型的 Pet 进行计数,Counter 继承了 HashMap 并预加载了 PetCreator.ALL_TYPES 里的类型。如果不预加载 Map 中的数据,最终就只能对随机生成的类型进行计数,而不能包括诸如 Pet 和 Cat 这样的基类型。

使用 isInstance() 方法,如果想添加新的 Pet 类型,只需要修改 PetCreator.types 数组就行了,其他部分不用修改。

递归计数

PetCount3.Counter 中的 Map 预先加载了所有不同的 Pet 类。我们可以使用 Class.isAssignableFrom() 而不是预加载 Map ,并创建一个不限于计数 Pet 的通用工具:

count() 方法获取其参数的 Class,并使用 isAssignableFrom() 在运行时验证传递的对象实际上在不在我们希望的层次结构里。countClass() 首先对这个确切类型进行计数,然后,如果其基类可以赋值给 baseType,则对基类进行递归调用 countClass()

注册工厂

通过 Pet 层次结构来生成对象存在一个问题,即每次向层次结构中添加新类型的 Pet 时,都必须记住将其添加到 PetCreator.java 的列表里。

你可能会考虑为每一个子类添加一个静态初始化器,这样初始化程序就可以将它的类添加到某个列表中。但是,静态初始化器只在类第一次加载时调用,所以就会遇到:生成器在它的列表中没有这个类,它永远不能创建这个类的对象,所以类不会被加载并放置到列表中。

基本上,必须自己手动创建这个列表(除非你编写了一个工具来搜索和分析源代码,然后创建和编译列表)。所以最佳的做法就是把这个列表放在位置明显的地方。层次结构的基类可能是最好的地方。

要做的另一个更改是使用工厂方法(Factory Method)设计模式来推迟对象的创建,将其交给类自己去完成。工厂方法可以以多态方式调用,来创建适当类型的对象。实际上,java.util.function.Supplier 通过它的 T get() 方法提供了一个工厂方法的原型。get() 方法可以通过协变返回类型为 Supplier 的不同子类返回对应的类型。

在该示例中,基类 Part 包含了一个工厂对象(Supplier<part>)的静态 List。对于本应该由 get() 方法生成的类型,它们的工厂类都被添加到列表 prototypes 里,从而“注册”到了基类中。较为特殊的是,这些工厂是对象本身的实例。这个列表中的每个对象都用于创建其他对象的原型

并不是所有的类都应该被实例化,该示例中的 Filter 和 Belt 只是用来分类的,不用创建它们的实例,只需创建它们子类的实例(若尝试创建,只会得到基类 Part 的行为)。

Part 实现了 Supplier<Part>,所以它可以通过自己的 get() 提供其他的 Part 对象。若调用了基类 Part 的 get() 方法(或通过 generate() 调用 get()),他会随机创建特定的 Part 子类型,每个子类型最终都继承自 Part, 并重写了 get() 方法来生成自身的对象。

Instanceof 与 Class 的等价性

当查询类型信息时,instanceofisInstance() 的效果是一样的,但它们与 Class 对象的直接比较存在重要区别。下面的例子展示了这种区别:

test() 方法使用两种形式的 instanceof 对其参数执行类型检查。然后,它获取 Class 引用,并使用 ==equals() 测试 Class 对象的相等性。从结果中可知,instanceofisInstance() 产生的结果相同, equals()== 产生的结果也相同。但测试本身得出了不同的结论。与类型的概念一致,instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的 Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。

运行时的类信息

如果不知道某个对象的确切类型,instanceof 可以告诉你。但是有一个限制:只有在编译时就知道的类型才能使用 instanceof 来检测,然后用获得的信息做一些有用的事。换句话说,编译器必须知道你使用的所有类。

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

重要的是要意识到反射没有什么魔力。当你使用反射与未知类型的对象交互时,JVM 将查看该对象,确定它属于哪个特定的类。在对其执行任何操作之前,必须加载对应的 Class 对象。因此,对于 JVM 来说,该特定类型的 .class 文件必须是可用的:要么在本地机器上,要么可以通过网络获得。通过反射,在编译时不可用的 .class 文件就可以在运行时被打开和检查了。

类方法提取器

通常不会直接用到反射工具,但它有助于创建更动态的代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见进阶篇附录)。但是,有时动态提取有关类的信息很有用。

考虑一个类方法提取器。当我们查看一个类定义的源代码或其 JDK 文档时,只能找到在该类中被定义或被重写的方法。但是,可能还有更多继承自基类的可用方法,要找这些方法在过去即乏味又费时。反射提供了一种方式,让我们能够编写简单的工具来自动展示完整的接口:

Class 类里的方法 getMethods()getConstructors() 分别返回了 Method 对象的数组和 Constructor 对象的数组。这两个类都提供了对应的方法,来进一步解析它们所代表的方法,并获取其名称、参数和返回值的相关信息。上面示例只是用 toString() 方法生成含有完整方法签名的字符串。代码的其余部分提取了命令行信息,判断某个特定的方法签名是否与我们的目标字符串相匹配(使用 contains()),并用正则表达式去掉了名称限定符。

可以注意到,即使代码中没有定义任何构造器,输出中也出现了一个 public 的无参构造器。若将 ShowMethods 设为非 public 类(即包访问权限),那么该自动合成的无参构造器就不会显示了。合成的无参构造器会自动获得与类相同的访问权限。

可以尝试运行带有 char, int, String 等额外参数的 CLI arguments。

动态代理

代理(proxy) 是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作---这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。

consumer() 方法接受一个 Interface 参数,所以它不知道自己得到的是一个 RealObject 还是一个 SimpleProxy,两者都实现了 Interface 接口。SimpleProxy 被插入到客户端和 RealObject 之间来执行操作,然后调用 RealObject 的相同方法。

如果想要将额外的操作从“实际”对象中分离,或者轻松启用这些额外的操作,代理就有用了(设计模式的关注点就是封装修改——因此需要做对应的操作来适应模式)。例如,如果你想跟踪对 RealObject 中方法的调用,或衡量此类调用的开销,该怎么办?你不想这部分代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它。

Java 的动态代理(dynamic proxy) 比代理更进一步,它可以动态地创建代理,并动态地处理对所代理方法的调用。在动态代理上进行的所有调用都会被重定向到一个调用处理程序(invocation handler)上,该处理程序负责发现调用的内容并决定如何处理。这是 SimpleProxyDemo.java 使用动态代理重写的例子:

通过调用静态方法 Proxy.newProxyInstance() 来创建动态代理,他需要三个参数:一个类加载器(通常可以从已加载的类中获取),一个希望代理实现的接口列表(不是类或抽象类),以及 InvocationHandler 接口的一个实现。动态代理会将所有调用重定向到调用处理程序,因此调用处理程序的构造器通常会获得“实际”对象的引用,以便它在执行完自己的中间任务后可以转发请求。

invoke() 方法被传递给代理对象,以防万一你必须区分请求的来源---但是在很多情况下都无需关心。但是,在 invoke() 内的代理上调用方法时要小心,因为接口的调用是通过代理重定向的。

通常,执行被代理的操作,然后使用 Method.invoke() 方法将请求转发给被代理的对象,并传入必要的参数。乍一看好像只能执行一般的操作。但是,可以过滤某些方法调用,同时放行其他方法调用:

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

动态代理不是你每天都会使用的工具,但是它可以很好地解决某些类型的问题。(了解更多关于代理和其它设计模式的信息:Erich Gamma "设计模式:可复用面向对象软件基础")

使用 Optional

当使用 null 来表示对象不存在时,为了安全,必须每次使用对象的引用时都测试它是否为 null。而且 null 没有自己的行为,当你尝试用它做任何事情时,都会产生一个 NullPointerException。在流章节中已经介绍过 java.util.Optional,它创建了一个简单的代理来屏蔽潜在的 null 值。Optional 对象会阻止你的代码直接抛出 NullPointerException。

虽然 Optional 是 Java 8 为了支持流式编程才引入的,但其实它是一个通用的工具。

Optional 在那些“更接近数据”的地方最有用,此时对象代表问题空间中的实体。举个简单的例子,很多系统中都有 Person 类型,代码中有些情况下你可能没有一个实际的 Person 对象(或者可能有,但是你还没用关于那个对象的所有信息)。这时,在传统方法下,你会用到一个 null 引用,并且在使用的时候测试它是不是 null。而现在,我们可以使用 Optional:

Person 的设计也被称为“数据传输对象”。注意,所有字段都是 public 和 final 的,因此没有 getter 和 setter 方法。即 Person 是不可变的,只能通过构造器给它赋值,之后就只能读而不能修改它的值(字符串本身就是不可变的,因此你无法修改字符串的内容,也无法给它的字段重新赋值)。如果你想修改一个 Person,你只能用一个新的 Person 对象来替换它。empty 字段在对象创建的时候被赋值,用于快速判断这个 Person 对象是不是空对象。

如果想使用 Person,就必须使用 Optional 接口才能访问它的 String 字段,这样就不会意外触发 NullPointerException 了。

假设你要招聘人员,但是职位空缺时。可以用 Optional 来为 Position 的 Person 字段提供占位符:

该示例使用了不同的方式来使用 Optional:修改字段只能通过 setTitle()setPerson(),而这两者都使用了 Optional 的功能对字段加以限制。

我们想保证 title 字段永远不会变成 null 值。为此,我们可以自己在 setTitle() 方法里边检查参数 newTitle 的值。但函数式编程一大优势就是可以让我们重用经过验证的功能(即便是个很小的功能),以减少自己手动编写代码可能产生的一些小错误。所以在这里,我们用 ofNullable() 把 newTitle 转换一个 Optional(如果传入的值为 null,ofNullable() 返回的将是 Optional.empty())。紧接着我们调用了 orElseThrow() 方法,所以如果 newTitle 的值是 null,你将会得到一个异常。这里我们并没有把 title 保存成 Optional,但通过应用 Optional 的功能,我们仍然如愿以偿地对这个字段施加了约束。

这里的 EmptyTitleException 是一个 RuntimeException,因为它代表了一个程序错误。该错误只有当 null 被传递给 setTitle() 时,即发生错误时才会出现,该异常的使用有助于进一步定位错误。

对于 Position,我们没有创建一个表示“空”的标志位或者方法,因为 person 字段的 Person 对象为空,就表示这个 Position 是个空缺位置。稍后你可能会必须在此添加一些明确的内容,但是根据 YAGNI 原则,只做最简单可行的事,直到程序要求添加额外的功能。(即不要过度设计)

注意:Staff 类轻松忽略了 Optional 的存在,尽管你知道它们在那里,保护你免受 NullPointerException 的影响。

注意,在有些地方你可能还是要测试引用是不是 Optional,这跟检查是否为 null 没什么不同。但是在其它地方(例如本例中的 toString() 转换),你就不必执行额外的测试了,而可以直接假设所有对象都是有效的。

标签接口

有时使用标签接口(tagging interface) 来表示可空性更方便。标签接口没有元素,只是将它的名称当作标签来使用:

如果使用的是接口而不是具体类,就可以使用 DynamicProxy 来自动生成 Null。假设有一个 Robot 接口,它定义了名称、模型以及一个描述了自身功能的 List<Operation>:

可以通过调用 operation() 来访问 Robot 服务。Robot 还包含了一个静态方法来执行测试。

Operation 包含了一个描述和一个命令(这是一种命令模式(Command pattern))。它们被定义为对函数式接口的引用,这样就可以将 lambda 表达式或方法引用传递给 Operation 的构造器:

现在可以创建一个扫雪的 Robot:

假设存在不同类型的 Robot,对于每种 Robot 类型,如果为 Null,则做一些特殊操作:本例中会提供 Robot 的确切类型信息,此信息由动态代理捕获:

每当需要一个空的 Robot 对象时,就可以调用 newNullRobot(),传递给它想要的 Robot 类型,他会返回一个代理。代理会同时满足 Robot 和 Null 接口的要求,并提供它所代理的类型的特定名称。

模拟对象和桩

模拟对象(Mock Object)和(Stub)是 Optional 的逻辑变体。他们都是最终程序中所使用的“实际”对象的代理。模拟对象和桩都假装是提供真实信息的实际对象,而不会像 Optional 那样隐藏对象(包括 null 对象)。

模拟对象和桩之间的区别在于程度不同。模拟对象往往是轻量级和自测试的,通常,为了处理各种不同的测试情况,会创建许多模拟对象。而桩只是返回桩数据,它通常是重量级的,并且经常在多个测试中被复用。桩可以根据它们被调用的方式,通过配置进行修改。因此,桩是一种复杂对象,它只做一件事情。如果需要做许多事情,通常会创建许多小而简单的模拟对象。

接口和类型消息

interface 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。先从一个接口开始:

然后实现这个接口:

通过反射,可以发现 a 实际上是被当作 B 实现的。通过强制转型为 B,可以调用不在 A 中的方法。

这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。

一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。这在很多情况下都是可行的,但“可能”还不够,你或许希望能有一些更严格的控制方式。

最简单的方法是使用包访问权限实现,这样包外的客户就看不到它了:

该包中唯一的 public 部分就是类 HiddenC,调用它会生成一个 A 接口,即使 makeA() 返回了一个 C 类型,在包外仍然不能使用除 A 外的任何事物,因为你不能在包外命名 C。

现在,如果尝试向下转型为 C,会发现找不到 C:

可以用反射来访问并调用所有方法(包括 private 方法)。如果已知方法的名称,就可以通过调用 Method 对象的 setAccessible(true) 来设置,从而让这个方法可以被调用,就像上面那样。

你可能觉得,可以通过只发布编译后的代码来阻止这种情况,但其实这并不能解决问题。因为只需要运行 javap(一个随 JDK 发布的反编译器)即可突破这一限制。下面是一个使用 javap 的命令行:

-private 标志表示所有的成员都应该显示,甚至包括私有成员。

因此,任何人都可以获取你最私有的方法的名字和签名,并调用它们。

若把接口实现为私有内部类:

匿名类:

没有任何方法可以阻止反射进入并调用非公共访问权限的方法。对于字段来说也是这样,即便是 private 字段:

可以看到,只有 final 字段没有发生变化。运行时系统会在不抛出异常的情况下接受任何修改的尝试,但是实际上不会发生任何修改。

总结

面向对象编程语言是想让我们尽可能地使用多态机制,只在非用不可的时候才使用反射。

然而使用多态机制的方法调用,要求我们拥有基类定义的控制权。因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来自别人的库,这时反射便是一种解决之道:可继承一个新类,然后添加你需要的方法。在代码的其它地方,可以检查你自己特定的类型,并调用你自己的方法。这样做不会破坏多态性以及程序的扩展能力,因为这样添加一个新的类并不需要修改程序中的 switch 语句。但如果想在程序中增加具有新特性的代码,你就必须使用反射来检查这个特定的类型。

将某个功能放在基类中可能意味着,为了某个特定类的利益,接口变得不那么合理。例如,考虑一个代表乐器的类层次结构,假设我们想清洁管弦乐队中某些乐器的排气阀。一个方法是在基类 Instrument 中放置一个 clearSplitValve() 方法,但这样会造成混淆,因为它暗示 Percussion, Stringed 和 Electronic 这些乐器也有排气阀。反射可以提供一个合理的解决方法,你可以将方法放在合适的特定类中(本例中为 Wind)。不过,在这里你可能会发现还有更好的解决方法,就是将 prepareInstrument() 放在基类中,但是初次面对这个问题的读者可能想不到还有这样的解决方案,而误认为必须使用反射。

最后更新于