11.5 反射
反射是在运行时审查、操作和修改对象的能力,可以修改对象的结构和行为,甚至还能自我修改。
即便编译时不知道类型和方法名称,也能使用反射。反射使用类对象提供的基本元数据,能从类对象中找出方法或字段的名称,然后获取表示方法或字段的对象。
(使用 Class::newInstance() 或另一个构造方法)创建实例时也能让实例具有反射功能。如果有一个能反射的对象和一个 Method 对象,我们就能在之前类型未知的对象上调用任何方法。
因此,反射是一种十分强大的技术,所以,我们要知道什么时候可以使用,什么时候由于功能太强而不能使用。
11.5.1 什么时候使用反射
很多,也许是多数 Java 框架都会适度使用反射。如果编写的架构足够灵活,在运行时之前都不知道要处理什么代码,那么通常都需要使用反射。例如,插入式架构、调试器、代码浏览器和 REPL 类环境往往都会在反射的基础上实现。
反射在测试中也有广泛应用,例如,JUnit 和 TestNG 库都用到了反射,而且创建模拟对象也要使用反射。如果你用过任何一个 Java 框架,即便没有意识到,也几乎可以确定,你使用的是具有反射功能的代码。
在自己的代码中使用反射 API 时一定要知道,获取到的对象几乎所有信息都未知,因此处理起来可能很麻烦。
只要知道动态加载的类的一些静态信息(例如,加载的类实现一个已知的接口),与这个类交互的过程就能大大简化,减轻反射操作的负担。
使用反射时有个常见的误区:试图创建能适用于所有场合的反射框架。正确的做法是,只处理当前领域立即就能解决的问题。
11.5.2 如何使用反射
任何反射操作的第一步都是获取一个 Class 对象,表示要处理的类型。有了这个对象,就能访问表示字段、方法或构造方法的对象,并将其应用于未知类型的实例。
获取未知类型的实例,最简单的方式是使用没有参数的构造方法,这个构造方法可以直接在 Class 对象上调用:
Class<?> clz = getSomeClassObject();Object rcvr = clz.newInstance();
如果构造方法有参数,必须找到具体需要使用的构造方法,并使用 Constructor 对象表示。
Method 对象是反射 API 提供的对象中最常使用的,下面会详细讨论。Constructor 和 Field
对象在很多方面都和 Method 对象类似。
1. Method对象
类对象中包含该类中每个方法的 Method 对象。这些 Method 对象在类加载之后惰性创建,所以在 IDE 的调试器中不会立即出现。
我们看一下 Method 类的源码,看看 Method 对象中保存了方法的哪些信息和元数据:
private Class<?> clazz;private int slot;// This is guaranteed to be interned by the VM in the 1.4// reflection implementationprivate String name;private Class<?> returnType;private Class<?>[] parameterTypes;private Class<?>[] exceptionTypesprivate int modifiers;// Generics and annotations supportprivate transient String signature;// Generic info repository; lazily initializedprivate transient MethodRepository genericInfo;private byte[] annotations;private byte[] parameterAnnotations;private byte[] annotationDefault;private volatile MethodAccessor methodAccessor;
Method 对象提供了所有可用信息,包括方法能抛出的异常和注解(保留 RUNTIME 异常的策略),甚至还有会被 javac 移除的泛型信息。
Method 对象中的元数据可以调用访问器方法查看,不过一直以来,Method 对象的最大用处是反射调用。
这些对象表示的方法可以在 Method 对象上使用 invoke() 方法调用。下面这个示例在 String 对象上调用 hashCode() 方法:
Object rcvr = "a";try {Class<?>[] argTypes = new Class[] { };Object[] args = null;Method meth = rcvr.getClass().getMethod("hashCode", argTypes);Object ret = meth.invoke(rcvr, args);System.out.println(ret);} catch (IllegalArgumentException | NoSuchMethodException |SecurityException e) {e.printStackTrace();} catch (IllegalAccessException | InvocationTargetException x) {x.printStackTrace();}
为了获取想使用的 Method 对象,我们在类对象上调用 getMethod() 方法,得到的是一个 Method 对象的引用,指向这个类中对应的公开方法。
注意,变量 rcvr 的静态类型是 Object。在反射调用的过程中不会用到静态类型信息。invoke() 方法返回的也是 Object 对象,所以 hashCode() 方法真正的返回值被自动打包成了 Integer 类型。
从自动打包可以看出,反射 API 有些方面稍微有点难处理——下一节详述。
2. 反射的问题
Java 的反射 API 往往是处理动态加载代码的唯一方式,不过 API 中有些让人头疼的地方,处理起来稍微有点困难:
大量使用
Object[]表示调用参数和其他实例;大量使用
Class[]表示类型;同名方法可以重载,所以需要维护一个类型组成的数组,区分不同的方法;
不能很好地表示基本类型——需要手动打包和拆包。
void 就是个明显的问题——虽然有 void.class,但没坚持用下去。Java 甚至不知道 void 是不是一种类型,而且反射 API 中的某些方法使用 null 代替 void。
这很难处理,而且容易出错,尤其是稍微有点冗长的数组句法,更容易出错。
处理非公开方法的方式是更大的问题。我们不能使用 getMethod() 方法,必须使用 getDeclaredMethod() 方法才能获取非公开方法的引用,而且还要使用 setAccessible() 方法覆盖 Java 的访问控制子系统,然后才能执行非公开方法:
public class MyCache {private void flush() {// 清除缓存……}}Class<?> clz = MyCache.class;try {Object rcvr = clz.newInstance();Class<?>[] argTypes = new Class[] { };Object[] args = null;Method meth = clz.getDeclaredMethod("flush", argTypes);meth.setAccessible(true);meth.invoke(rcvr, args);} catch (IllegalArgumentException | NoSuchMethodException |InstantiationException | SecurityException e) {e.printStackTrace();} catch (IllegalAccessException | InvocationTargetException x) {x.printStackTrace();}
不过,需要指出的是,使用反射的过程中始终会涉及未知信息。从某种程度上看,为了能处理反射调用,为了能使用反射 API 为开发者提供的运行时动态功能,我们只能容忍这种啰嗦的方式。
下面是本节最后一个示例。这个示例把反射和自定义类加载结合在一起使用,检查硬盘中的类文件里是否包含弃用方法(弃用方法应该使用 @Deprecated 标记):
public class CustomClassloadingExamples {public static class DiskLoader extends ClassLoader {public DiskLoader() {super(DiskLoader.class.getClassLoader());}public Class<?> loadFromDisk(String clzName)throws IOException {byte[] b = Files.readAllBytes(Paths.get(clzName));return defineClass(null, b, 0, b.length);}}public void findDeprecatedMethods(Class<?> clz) {for (Method m : clz.getMethods()) {for (Annotation a : m.getAnnotations()) {if (a.annotationType() == Deprecated.class) {System.out.println(m.getName());}}}}public static void main(String[] args)throws IOException, ClassNotFoundException {CustomClassloadingExamples rfx =new CustomClassloadingExamples();if (args.length > 0) {DiskLoader dlr = new DiskLoader();Class<?> clzToTest = dlr.loadFromDisk(args[0]);rfx.findDeprecatedMethods(clzToTest);}}}
