# 反射

反射是 Java 的特有概念, 简单来说就是允许程序在运行过程获得甚至修改类及类内部的相关信息. 总的来说, 反射允许我们从一个特别的角度了解并操纵类与对象.

WARNING

反射的部分还缺少泛型的解释与说明, 以及数组的解释与说明

# 为什么要有反射

面向对象编程应该努力做到只操纵基类的引用, 保证大部分代码尽可能少的了解对象的具体类型. 然后我们想想 Java 中的多态, 我们很多时候都不会在代码中明确说明 某个引用具体指引着哪个类的对象, 更多的只会写下基类的引用.

那么问题来了, JVM 在执行类加载的时候, 如何知道某一个引用具体指向的是哪个类的对象? 又比如, 在泛型程序设计或者框架设计中, 如何创建一个更加通用的程序(使用 if 判断注入的对象通常是不明智的).

# 反射的基本原理

# Java 代码的处理过程

在程序运行期间, Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识. 这个信息跟踪着每个对象所属的类. JVM 利用运行时类型信息选择相应的方法执行.

捋一捋上面这句话, 先只考虑最简单的情况, 一个类包括类构造器与类属性和类方法, 类构造器和类方法又包括方法签名和逻辑代码.

# 编写代码

在使用的时候, 我们先从静态方法 main() 开始, 想办法创建对象, 以及这些对象对应的引用, 然后调用方法或者属性编写逻辑. 我们编写好的程序会被存放到硬盘(或者别的什么地方)上等待 JVM 的加载.

# 编译

如果我们下达了执行命令, JVM 就会编译 .java 文件将其转为 .class 文件. 这个过程中, 代码内每一个我们创建的类都会拥有一个与自己同名的 .class 文件用来记录类的内部信息.

# 加载

这些文件被 JVM 加载后就会成为 Class 对象(属于 Class 类), 这些对象将包含类属性集合对象(Field[] 对象), 类构造器集合对象(Constructor[] 对象), 类方法集合对象(Method[] 对象).

# 总结

从上面可以看出, 一切的一切都指向了 java.lang.Class 类. 此外还有 java.lang.reflect.Field, java.lang.reflect.Method, java.lang.reflect.Constructor;

# 简单地看一下与反射相关的包与 API

# Object 类

  • public final native Class<?> getClass()
    Java 中所有的类都会继承 Object 类, 而 Object 类存在方法 getName(), 那么可以使用此方法获得当前对象对应的 Class 对象的引用.

# Class 类

  • public String getName()
    获得当前 Class 对象的名称.
  • public static Class<?> forName(String className)
    通过 Class 对象的名称(加上包名)获得相应的 Class 对象的引用.
  • public T newInstance()
    这个方法会可以返回一个当前 Class 对象对应的 T 类对象的引用. 但是 T 类中应该包含一个默认构造器, 否则将报错.
  • public Field[] getFields() / public Field[] getDeclareFields()
    返回一个类对象中所有公有的/全部的 Field 对象的引用
  • public Method[] getMethods() / public Method[] getDeclareMethods()
    返回一个类对象中所有公有的/全部的 Method 对象的引用
  • public Constructor<?>[] getConstructors() / public Constructor<?>[] getDeclareConstructors()
    返回一个类对象中所有公有的/全部的 Constructor 对象的引用

# 大部分的方法用起来都差不多, 下面给出一个示例:

package lang;

class Test {
    public void func() {
        System.out.println("hello this is func");
    }
}

public class ClassTest {
    public static void main(String[] args) throws 
        ClassNotFoundException, IllegalAccessException, InstantiationException 
    {
        // 获取 Test 类对应的 Class 对象的引用 testClass
        Class testClass = Class.forName("lang.Test");
        // 创建 Test 类对象
        Test test = (Test)testClass.newInstance();
        test.func();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 反射的作用

底下是一些常见的反射功能.

# 分析类

前面提到过 java.lang.reflect.Field, java.lang.reflect.Method, java.lang.reflect.Constructor 这三个类分别对应了类的域, 方法和构造方法. 三个类的使用和 Class 类也非常类似. 还可以获得类成员的访问限制情况. 但是, 这三个类没有直接获取基类成员的能力.

# 分析对象

在运行时, JVM 会以类为模板创造对象, 每个对象都有自己的状态, 那么这一节就专门讨论如何获得与修改对象状态, 同时, 进行这类操作还会受到 Java 的权限控制的影响.

下面的代码中, 我们会干这么几件事:

  1. 创建一个 Test 类对象.
  2. 获得这个 Test 类 对应的 Class 类对象 (可能会有点绕) , 有了 Class 类对象, 由此打开了新世界的大门.
  3. 获取 Test 类中的成员属性 对应的 Field 类对象.
  4. 修改访问限制.
  5. 获取成员属性并打印.
  6. 修改成员属性并打印.
class Test {
    private int a = 12;
    public void func() {
        System.out.println(this.a);
    }
}

public class ClassTest {
    public static void main(String[] args) throws
            NoSuchFieldException, IllegalAccessException 
    {
        Test test = new Test();
        Class testClass = test.getClass();
        Field field = testClass.getDeclaredField("a");

        field.setAccessible(true);
        int a = (int) field.get(test);
        test.func();
        field.set(test, 16);
        test.func();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面的代码中提到了三个方法:

  • AccessibleObject 类下的 public void setAccessible(boolean flag)
    跨过访问限制
  • Field 类下的 public Object get(Object obj)
    获取成员属性的状态
  • Field 类下的 public void set(Object obj, Object value)
    修改成员属性的状态

TIP

get 和 set 方法中输入的 Object 对象是需要获取/修改属性的对象, 这里是 Test 对象; 而返回值的 Object 就是属性的值, 如果是基本类型, 反射会自动处理. 这种写法看着可能有些奇怪, 因为每个类都可能有多个对象, 也就存在多组属性状态, 必须告知 get/set 需要处理的是具体哪个对象下的属性.

# 调用方法

还是之前那个例子:

class Test {
    private int a = 12;
    public void func() {
        System.out.println(this.a);
    }
}

public class ClassTest {
    public static void main(String[] args) throws
            IllegalAccessException, NoSuchMethodException, InvocationTargetException 
    {
        Test test = new Test();
        Class testClass = test.getClass();
        Method method = testClass.getDeclaredMethod("func");
        method.invoke(test);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这里出现了一个不太一样的方法: public Object invoke(Object obj, Object... args) , 它对应之前看到的 set 和 get 方法. 他同样需要具体的对象引用 test 作为参数, 如果调用的方法需要参数化, 那么还需要给出具体的参数.

# 总结

在最初学习 Java 的时候, 我认为反射是一个建设框架才需要学习的知识, 后来接触到了一些后端框架, 才慢慢意识到了解反射的重要性.

总的来说, 反射是允许我们在 JVM 层面以上 "为所欲为" 的基础. 但是 "劲酒不能贪杯" , 当有其他备选的而且几乎等效的手段, 就应该避免使用反射. 不正确的反射会带来一些难以察觉的错误.

Last Updated: 12/23/2019, 7:43:35 AM