11.5 反射

反射是在运行时审查、操作和修改对象的能力,可以修改对象的结构和行为,甚至还能自我修改。

即便编译时不知道类型和方法名称,也能使用反射。反射使用类对象提供的基本元数据,能从类对象中找出方法或字段的名称,然后获取表示方法或字段的对象。

(使用 Class::newInstance() 或另一个构造方法)创建实例时也能让实例具有反射功能。如果有一个能反射的对象和一个 Method 对象,我们就能在之前类型未知的对象上调用任何方法。

因此,反射是一种十分强大的技术,所以,我们要知道什么时候可以使用,什么时候由于功能太强而不能使用。

11.5.1 什么时候使用反射

很多,也许是多数 Java 框架都会适度使用反射。如果编写的架构足够灵活,在运行时之前都不知道要处理什么代码,那么通常都需要使用反射。例如,插入式架构、调试器、代码浏览器和 REPL 类环境往往都会在反射的基础上实现。

反射在测试中也有广泛应用,例如,JUnit 和 TestNG 库都用到了反射,而且创建模拟对象也要使用反射。如果你用过任何一个 Java 框架,即便没有意识到,也几乎可以确定,你使用的是具有反射功能的代码。

在自己的代码中使用反射 API 时一定要知道,获取到的对象几乎所有信息都未知,因此处理起来可能很麻烦。

只要知道动态加载的类的一些静态信息(例如,加载的类实现一个已知的接口),与这个类交互的过程就能大大简化,减轻反射操作的负担。

使用反射时有个常见的误区:试图创建能适用于所有场合的反射框架。正确的做法是,只处理当前领域立即就能解决的问题。

11.5.2 如何使用反射

任何反射操作的第一步都是获取一个 Class 对象,表示要处理的类型。有了这个对象,就能访问表示字段、方法或构造方法的对象,并将其应用于未知类型的实例。

获取未知类型的实例,最简单的方式是使用没有参数的构造方法,这个构造方法可以直接在 Class 对象上调用:

  1. Class<?> clz = getSomeClassObject();
  2. Object rcvr = clz.newInstance();

如果构造方法有参数,必须找到具体需要使用的构造方法,并使用 Constructor 对象表示。

Method 对象是反射 API 提供的对象中最常使用的,下面会详细讨论。ConstructorField

对象在很多方面都和 Method 对象类似。

1. Method对象

类对象中包含该类中每个方法的 Method 对象。这些 Method 对象在类加载之后惰性创建,所以在 IDE 的调试器中不会立即出现。

我们看一下 Method 类的源码,看看 Method 对象中保存了方法的哪些信息和元数据:

  1. private Class<?> clazz;
  2. private int slot;
  3. // This is guaranteed to be interned by the VM in the 1.4
  4. // reflection implementation
  5. private String name;
  6. private Class<?> returnType;
  7. private Class<?>[] parameterTypes;
  8. private Class<?>[] exceptionTypes
  9. private int modifiers;
  10. // Generics and annotations support
  11. private transient String signature;
  12. // Generic info repository; lazily initialized
  13. private transient MethodRepository genericInfo;
  14. private byte[] annotations;
  15. private byte[] parameterAnnotations;
  16. private byte[] annotationDefault;
  17. private volatile MethodAccessor methodAccessor;

Method 对象提供了所有可用信息,包括方法能抛出的异常和注解(保留 RUNTIME 异常的策略),甚至还有会被 javac 移除的泛型信息。

Method 对象中的元数据可以调用访问器方法查看,不过一直以来,Method 对象的最大用处是反射调用。

这些对象表示的方法可以在 Method 对象上使用 invoke() 方法调用。下面这个示例在 String 对象上调用 hashCode() 方法:

  1. Object rcvr = "a";
  2. try {
  3. Class<?>[] argTypes = new Class[] { };
  4. Object[] args = null;
  5. Method meth = rcvr.getClass().getMethod("hashCode", argTypes);
  6. Object ret = meth.invoke(rcvr, args);
  7. System.out.println(ret);
  8. } catch (IllegalArgumentException | NoSuchMethodException |
  9. SecurityException e) {
  10. e.printStackTrace();
  11. } catch (IllegalAccessException | InvocationTargetException x) {
  12. x.printStackTrace();
  13. }

为了获取想使用的 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 的访问控制子系统,然后才能执行非公开方法:

  1. public class MyCache {
  2. private void flush() {
  3. // 清除缓存……
  4. }
  5. }
  6. Class<?> clz = MyCache.class;
  7. try {
  8. Object rcvr = clz.newInstance();
  9. Class<?>[] argTypes = new Class[] { };
  10. Object[] args = null;
  11. Method meth = clz.getDeclaredMethod("flush", argTypes);
  12. meth.setAccessible(true);
  13. meth.invoke(rcvr, args);
  14. } catch (IllegalArgumentException | NoSuchMethodException |
  15. InstantiationException | SecurityException e) {
  16. e.printStackTrace();
  17. } catch (IllegalAccessException | InvocationTargetException x) {
  18. x.printStackTrace();
  19. }

不过,需要指出的是,使用反射的过程中始终会涉及未知信息。从某种程度上看,为了能处理反射调用,为了能使用反射 API 为开发者提供的运行时动态功能,我们只能容忍这种啰嗦的方式。

下面是本节最后一个示例。这个示例把反射和自定义类加载结合在一起使用,检查硬盘中的类文件里是否包含弃用方法(弃用方法应该使用 @Deprecated 标记):

  1. public class CustomClassloadingExamples {
  2. public static class DiskLoader extends ClassLoader {
  3. public DiskLoader() {
  4. super(DiskLoader.class.getClassLoader());
  5. }
  6. public Class<?> loadFromDisk(String clzName)
  7. throws IOException {
  8. byte[] b = Files.readAllBytes(Paths.get(clzName));
  9. return defineClass(null, b, 0, b.length);
  10. }
  11. }
  12. public void findDeprecatedMethods(Class<?> clz) {
  13. for (Method m : clz.getMethods()) {
  14. for (Annotation a : m.getAnnotations()) {
  15. if (a.annotationType() == Deprecated.class) {
  16. System.out.println(m.getName());
  17. }
  18. }
  19. }
  20. }
  21. public static void main(String[] args)
  22. throws IOException, ClassNotFoundException {
  23. CustomClassloadingExamples rfx =
  24. new CustomClassloadingExamples();
  25. if (args.length > 0) {
  26. DiskLoader dlr = new DiskLoader();
  27. Class<?> clzToTest = dlr.loadFromDisk(args[0]);
  28. rfx.findDeprecatedMethods(clzToTest);
  29. }
  30. }
  31. }