Java 高级特性の反射

2,292 阅读3分钟

这是我参与8月更文挑战的第 15 天,活动详情查看:8月更文挑战

反射存在的意义:

有的操作在硬编码的时候,无法实现,必须要在运行的时候获取到相关参数才能使用

反射可以实现的功能

根据指定的 class,以及对应的实例 obj,来获取 obj 上的所有属性,调用 obj 的所有方法,以及构造器

典型的场景

spring 上古时代的版本,2.x,那个时候主流就是 xml 配置,比如下面一段,我们配置了 User.class 的一个实例 user,spring 框架使用 class 属性中的文本描述,以 Class.forName(className) (或其他方式,来加载 User,然后根据 xml 里的配置生成一个 user 实例

<bean id="user" class="com.wb.bean.User">
</bean>

工作场景

有时候我们需要编写通用的代码,不太合适直接对指定类直接进行硬编码,或者是对这一系列的类型都无法使用泛型来处理

比如,我需要生成一个测试类,需要给他的字段随机赋值

或者,我需要复制一个实例,如果硬编码类似于 b.setName(a.getName()),就无法做到通用性(而且代码又会显得很呆)

亦或者,我有 2 套数据模版,需要根据某种规则,从 classA 的实例映射为 ClassB 实例(这个后面出一篇文章仔细聊)

代码演示

演示一下我们工作中反射的最最常见的使用

@ToString
// 本次演示用到的 bean
class User {
  String name;
  String job;
  User() {}
  private User(String name) {
    this.name = name;
  }
  private void sayHi(String prev) {
    System.out.printf("%s, I'm %s, %s is my job.", prev, name, job);
  }
}
// 使用 junit 进行测试
@Test
public void f2() throws Exception {
  // 获取到 class 的类型数据
  Class<?> clz = User.class;
  // Class<?> clz = Class.forName("com.wb.User"); // 如果我们的系统在运行之后从其他途径加载了 class 文件,我们可以使用根据文本描述的类路径来加载,比如,我们初学 jdbc 的时候就是这样加载 mysql 的 dirver
  // 根据参数列表来选择对应的构造器
  Constructor<?> oneParams = clz.getDeclaredConstructor(String.class);
  // 设置可以获取到私有构造器
  oneParams.setAccessible(true);
  // 通过构造器进行实例化
  Object obj = oneParams.newInstance("wangb");
  System.out.println(obj);  // 输出 User(name=wangb, job=null, dept=null)
  // 根据文本描述去获取 job 这个字段
  Field job = clz.getDeclaredField("job");
  // 设置可以访问私有属性
  job.setAccessible(true);
  // 进行赋值操作
  job.set(obj, "touch-fish");
  System.out.println(obj);  // 输出 User(name=wangb, job=touch-fish, dept=null)
  // 获取 clz 上的 sayHi(String) 方法
  Method sayHi = clz.getDeclaredMethod("sayHi", String.class);
  // 通用设置为获取私有方法
  sayHi.setAccessible(true);
  // 进行方法调用
  sayHi.invoke(obj, "morning"); // morning, I'm wangb, touch-fish is my job.
}

通过上面一个测试用例,我们会有一种很强烈的感觉

我们是站在一个上帝视角,知道可以获取哪些属性,使用哪些 方法、构造器,而这些东西,在我们 coding 的时候,是不存在的!

细节讲解

我们使用的反射相关的类,都位于 java.lang.reflect

通过 clz.getxxx 去获取属性、方法、构造器时,有 2 种可选项,以获取属性为例

  • Field name = clz.getDeclaredField(name);
  • Field name2 = clz.getField(name);

前者额外执行一步操作 name.setAccessible(true); 即可访问私有的字段,然后可以对 name 进行读写;后者,只能获取到 public 的数据

获取全部构造器

Constructor<?>[] constructors = clz.getConstructors();

获取全部方法

Method[] methods = clz.getDeclaredMethods();

获取全部字段

Field[] fields = clz.getDeclaredFields();

当我们需要获取到全部的属性时,一般会使用 foreach 循环进行遍历

如下示意:

for (Field f : fields) {
  // do somethings
}

最后

反射虽然说是高级特性,但是这 api 也没多少,重要是我们理解了反射能做什么事情之后,发现这并没什么难的,无外乎根据需要去绕两圈实现我们想要的东西

当然,需要说明的是,毕竟不是直接调用,反射会存在性能问题,如果需要经常根据反射去调用,最好缓存相关的 Fields, Methods,避免重复获取造成性能浪费

图片.png


原创文章,未经允许,禁止转载

-- by 安逸的咸鱼