这是我参与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,避免重复获取造成性能浪费
原创文章,未经允许,禁止转载
-- by 安逸的咸鱼