ITEM 65: 使用接口而不是反射
ITEM 65: PREFER INTERFACES TO REFLECTION
核心反射工具 java.lang.reflect 提供对任意类的编程访问。给定一个类对象,您可以获得表示类实例所表示的类的构造函数、方法和字段实例。这些对象提供对类的成员名、字段类型、方法签名等的编程访问。
此外,构造函数、方法和字段实例允许您反射性地操作它们的底层副本:您可以通过调用构造函数、方法和字段实例上的方法来构造实例、调用方法和访问底层类的字段。例如, Method.invoke 允许您在任何类的任何对象上调用任何方法(受通常的安全约束)。反射允许一个类使用另一个类,即使在编译前者时后者并不存在。然而,这种力量是有代价的:
• 您失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在或不可访问的方法,那么它将在运行时失败,除非您采取了特殊的预防措施。
• 执行反射访问所需的代码笨拙而冗长。写起来很乏味,读起来很困难。
• 性能。反射方法调用比普通方法调用慢得多。由于有许多因素在起作用,所以很难确切地说慢了多少。在我的机器上,调用一个没有输入参数和 int 返回的方法要慢11倍。
有一些复杂的应用程序需要反射。示例包括代码分析工具和依赖项注入框架。即使是这样的工具,随着它的缺点变得越来越明显,也在逐渐远离反思。如果您怀疑您的应用程序是否需要反射,那么它可能不需要。
仅在非常有限的形式中使用反射,可以获得反射的许多好处,同时花费很少。对于许多必须使用在编译时不可用的类的程序,在编译时存在一个适当的接口或超类,通过它来引用类(item 64)。如果是这种情况,您可以反射性地创建实例,并通过它们的接口或超类正常地访问它们。例如,这是一个创建 Set 实例的程序,其类由第一个命令行参数指定。程序将剩余的命令行参数插入集合并打印它。不管第一个参数是什么,程序都会打印剩余的参数,并消除重复。但是,打印这些参数的顺序取决于第一个参数中指定的类。如果您指定 java.util.HashSet,它们显然是随机排列的;如果您指定 java.util.TreeSet,它们是按字母顺序打印的,因为树集中的元素是排序的:
// Reflective instantiation with interface access
public static void main(String[] args) {
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
// Unchecked cast!
cl = (Class<? extends Set<String>>) Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
虽然这个程序只是一个玩具,但它演示的技术非常强大。示例程序可以很容易地转换成一个通用的集合测试器,通过积极地操作一个或多个实例并检查它们是否遵守集合契约来验证指定的集合实现。类似地,它可以变成一个通用的集性能分析工具。事实上,该技术足以实现一个成熟的服务提供者框架(item 1)。
这个例子说明了反射的两个缺点。首先,该示例可以在运行时生成 6 个不同的异常,如果没有使用反射实例化,所有这些异常都将是编译时错误 (为了好玩,您可以通过传入适当的命令行参数来让程序生成六个异常中的每一个)。第二个缺点是,从类名生成类实例需要 25 行冗长的代码,而构造函数调用只需一行即可。通过捕获ReflectiveOperationException (Java 7 中引入的各种反射异常的超类),可以减少程序的长度。这两个缺点都局限于实例化对象的程序部分。一旦实例化,该集合就与任何其他集合实例难以区分。在实际的程序中,大量的代码因此不受反射这种有限使用的影响。
如果您编译这个程序,您将得到一个未选中的强制转换警告。这个警告是合法的,因为对 Class 即使指定的类不是 Class<? extends Set<String>> 也会成功,在这种情况下,程序在实例化类时抛出 ClassCastException。要了解如何抑制警告,请阅读item 27。
反射的一个合理(如果比较少见)的用途是管理类对其他类、方法或字段的依赖关系,这些依赖关系在运行时可能不存在。如果您正在编写一个必须针对其他某个包的多个版本运行的包,那么这将非常有用。技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。要实现此功能,必须在运行时不存在要访问的新类或方法时采取适当的操作。
适当的行动可能包括使用一些替代方法来完成相同的目标,或者使用减少的功能进行操作。总之,反射是某些复杂的系统编程任务所需的强大工具,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射来实例化对象,并使用在编译时已知的接口或超类来访问对象。