第六十五条:接口优先于反射机制
核心反射机制,java.lang.reflect包,提供了通过程序来访问任意类的能力。给定一个Class对象,可以获得Constructor、Method和Field实例,它们分别代表了该Class实例所表示的类的构造器、方法和域。这些对象提供了通过程序来访问类的成员名称、域类型、方法签名等信息的能力。
此外,Constructor、Method和Field实例使你能够通过反射机制操作它们的底层对等体:通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的域。例如,Method.invoke使你可以调用任何类的任何对象上的任何方法(遵从常规的安全限制)。反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而,这种能力也要符出代价:
1、损失了编译时类型检查的优势
:包括异常的检查。如果程序企图用反射方式调用不存在或者不可访问的方法,在运行时它将会失败,除非采用了特别的预防措施。
2、执行反射访问所需要的代码非常笨拙和冗长
:编写这样的代码非常乏味,阅读起来也很困难。
3、性能损失
:反射方法调用比普通方法调用慢了许多。具体慢了多少,这很难说,因为受到了多个因素的影响。在我的机器上,调用一个没有输入参数和int返回值的方法,用普通方法调用比用反射机制调用快了11倍。
有些复杂的运用程序需要使用反射机制。这些示例包括代码分析工具和依赖注入框架。不过最近以来,这类工具已经不再使用反射机制,因为它的缺点越来越明显。如果你怀疑自己的应用程序是否也需要反射机制,它很有可能是不需要的。
如果只是以非常有限的形式使用反射机制,虽然也要符出少许代价,但是可以获得许多好处
。许多程序员必须用到的类在编译时是不可用的,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类(详见第64条)。如果是这种情况,就可以反射机制方式创建实例,然后通过它们的接口或者超类,以正常的方法访问这些实例
。
例如,下面的程序创建了一个Set<String>实例,它的类是由第一个命令行参数指定的。该程序把其余的命令行参数插入到集合中,然后打印该集合。不管第一个参数是什么,程序都会打印出余下的命令行参数,其中重复的参数会被消除掉。这些参数的打印顺序取决于第一个参数中指定的类。如果指定java.util.HashSet,显然这些参数就会以随机的顺序打印出来;如果指定java.util.TreeSet,则会按照字母顺序打印,因为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);
}
尽管这只是一个试验程序,但是它所演示的方法是非常强大的。这个试验程序可以很容易的变成一个通用的集合测试器,通过侵入式操作一个或者多个集合实例,并检查是否遵守Set接口的约定,以此来验证指定的Set实现。同样的,它也可以变成一个通用的集合性能分析工具。实际上,它所演示的这种方法足以实现一个成熟的服务提供者框架,详见第一条。绝大多数情况下,使用反射机制时需要的也正是这种方法。
这个示例演示了反射机制的两个缺点。第一,这个例子会产生6个运行时异常,如果不使用反射机制方式的实例化,这6个错误都会变为编译时错误。第二,根据类名生成其实例需要25行冗长的代码,而调用一个构造器则可以非常简洁的只用一行代码。程序的长度可以通过捕捉ReflectiveOperationException异常来减少,这是在Java7中引入的各种反射异常的一个超类。这两个缺点都局限于实例化对象的那部分代码。一旦对象被实例化,它与其他的Set实例就难以区分了。在实际的程序中,通过这种限定使用反射的方法,绝大部分代码可以不受影响。
如果试着编译这个程序,会得到一条未受检的转换警告。这条警告是合法的,因为转换Class<? extends Set<String>>会成功,即使具名类不是一个Set实现,在这种情况下,程序在实例化这个类时就会抛出一个ClassCastException异常。要了解禁止这种警告的最佳方法,请参考第27条。
类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射进行管理是合理的,但是很少使用。如果要编写一个包,并且它运行的时候就必须依赖其他某个包的多个版本,这种做法可能就非常有用。具体做法就是,在支持包所需要的最小环境下对它进行编译,通常是最老的版本,然后以反射方式访问任何更加新的类或者方法。如果企图访问新的类或者新方法在运行时不存在,为了使这种方法有效,你还必须采取适当的动作。所谓适当的动作,可能包括使用某种其他可替换的办法来达到同样的目的,或者使用简化的功能进行处理。
总而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但是它也有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类
。