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