Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java
核心反射工具java.lang.reflect提供对任意类的编程访问。 给定一个Class对象,能够得到Constructor,Method和Field实例,这些实例表示由Class实例表示的类的构造方法,方法和属性。 这些对象提供对类的成员名称,属性类型,方法签名等的编程访问。git
此外,Constructor,Method和Field实例容许反射性地操做它们的底层对应物:能够经过在Constructor,Method和Field实例上调用方法来构造实例,调用方法和访问底层类的属性。 例如,Method.invoke方法容许在任何类的任何对象上调用任何方法(受一般的安全性约束)。 反射容许一个类使用另外一个类,即便在编译前者时后者类并不存在。 然而,这种能力是有代价的:github
失去了编译时类型检查的全部好处,包括异常检查。若是一个程序试图经过反射调用一个不存在的或不可访问的方法,会在运行时失败,除非采起了特殊的预防措施。编程
执行反射访问所需的代码笨拙而冗长。写起来很乏味,读起来很困难。安全
性能受损。反射方法调用比普通方法调用慢得多。到底慢了多少还很难说,由于有不少因素在起做用。在个人机器上,调用一个没有输入参数和返回int类型的方法时,反射方法执行要比普通方法慢11倍。框架
有一些复杂的应用程序须要反射。示例包括代码分析工具和依赖注入框架。即便是这样的工具,随着它的缺点变得愈来愈明显,也在逐渐远离反射。若是你对应用程序是否须要反射有任何疑问,那么它多是不须要的。less
经过以很是有限的形式使用反射,能够得到反射的许多好处,同时花费不多。对于许多必须使用在编译时不可用的类的程序,在编译时存在一个适当的接口或父类来引用该类( 条目64)。若是是这种状况,可使用反射建立实例,并经过它们的接口或父类正常地访问它们。ide
例如,这是一个建立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 { cl = (Class<? extends Set<String>>) // Unchecked cast! 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实现。 一样,它能够变成泛型集合性能分析工具。 事实上,这种技术足以实现一个成熟的服务提供者框架(service provider framework)(条目 1)。 一般,这种技术就是你在反射中所须要的。工具
这个例子说明了反射的两个缺点。 首先,该示例在运行时生成六个不一样的异常,若是不使用反射实例化,则全部这些异常都是编译时错误。 (为了好玩,能够经过传入适当的命令行参数使程序生成六个异常中的每个)。第二个缺点是须要25行繁琐的代码才能从其名称生成类的实例, 而构造函数方法使用一行代码便可。 能够经过捕获ReflectiveOperationException
来减小程序的长度,该异常是Java 7中引入的各类反射异常的父类。这两个缺点仅限于实例化对象的程序部分。 实例化后,该集合与任何其余Set实例没法区分。 在真实的程序中,大量的代码所以不会受这种限定的反射使用的影响。
若是编译此程序,会得到未经检查的强制转换警告。 这个警告是合法的, 所以转换Class<? extends Set<String>>
也会成功,即便指定的集合不是Set
接口的实现。在这种状况下,程序在实例化类时抛出ClassCastException
异常。 要了解有关抑制警告的信息,请阅读条目 27。
反射的合法(若是不多)用途是管理类对运行时可能不存在的其余类、方法或属性的依赖关系。若是你正在编写一个必须针对其余包的多个版本运行的包,这将很是有用。该技术是根据支持包所需的最小环境(一般是最老的版本)编译包,并反射访问任何较新的类或方法。要使此工做正常进行,若是试图访问的新类或方法在运行时不存在,则必须采起适当的操做。适当的行动可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操做。
总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,可是它有不少缺点。若是编写的程序必须在编译时处理未知的类,则应该尽量只使用反射实例化对象,并使用在编译时已知的接口或父类访问对象。