第53条:接口优先于反射机制
核心反射机制:java.lang.reflect包 提供了“通过程序来访问关于已装载的类的信息”的能力,给定一个Class实例,可以获得Constructor、Method、Field实例,这些对象提供“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,存在的代价:
1.失去编译时类型检查的好处,包括异常检查。
2.执行反射访问所需的代码很长。 编写这样的代码非常乏味,阅读起来也很困难。
3.性能上的损失。反射方法调用比普通方法调用慢了许多。
反射功能只是在设计时被用到,通常,普通应用程序在运行时不应该以反射的方式访问对象。
有些复杂的应用程序需要使用反射机制,包括类浏览器、对象检测器、代码分析工具、解释型的内嵌式系统。在RPC中使用反射机制也是合适的,这样就不再需要存根编译器。
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类(见第52条:通过接口引用对象)。如果是这种情况,就可以以反射方式创建实例, 然后通过它们的接口或者超类,以正常的方式访问这些实例。如果适当的构造器不带参数,甚至根本不需要使用java.lang.reflect包,Class.newlnstance方法就已经提供了所需的功能。
下面的程序创建了一个Set实例,它的类是由第一个命令行参数指定的。 该程序把其余的命令行参数插入到这个集合中,然后打印该集合。不管第一个参数是什么,程序都会打印出余下的命令行参数,其中重复的参数会被消除掉。这些参数的打印顺序取决于第一个参数中指定的类。如果指定“java.util.HashSet”,显然这些参数就会以随机的顺序打印出来,如果指定“java.util.TreeSet”,则它们就会按照字母顺序打印出来,因为TreeSet中的元素是排好序的。相应的代码如下:
public static void main(String[] args) {
Class<?> c = null;
try {
c = Class.forName(args[0]);
} catch(ClassNotFoundException e) {
System.out.println("Class not found");
System.exit(1);
}
Set<String> s = null;
try {
s = (Set<String>) c.newInstance();
} catch(IllegalAccessException e) {
System.out.println("Class not accessible");
System.exit(1);
} catch(InstantiationException e) {
System.out.println("Class not instantiable");
System.exit(1);
}
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
它所演示的这种方法是非常强大的,它可以很容易地变成一个通用的集合测试器,通过侵入式地操作一个或者多个集合实例,并检査是否遵守Set接口的约定,以此来验证指定的Set实现。同样地,它也可以变成一个通用的集合性能分析工具。实际上,它所演示的这种方法足以实现一个成熟的服务提供者框架 ( 见第1条:考虑用静态工厂方法代替构造器)。
同时它也暴露了反射机制的两个缺点:
1.这个例子会产生3个运行时错误,如果不使用反射方式的实例化,这3个错误都会成为编译时错误。
2.根据类名生成它的实例需要20行冗长的代码,而调用一个构造器可以非常简洁地只使用一行代码。
总之,对于复杂的系统编程任务,反射是必要的,如果编写的程序必须与编译时未知的类一起工作,如果可能,就应该仅仅使用放射机制来实例化对象,而访问对象时则用编译时已知的某个接口或者超类。