反射机制
反射概述
什么是反射
把“一个类的结构信息”当成数据来读取/操作,这件事就叫 反射。
更具体一点:Java 会把类的“组成部分”封装成一些对象,例如:
- 成员变量 →
Field - 构造方法 →
Constructor - 成员方法 →
Method
给前端的 30 秒理解
如果你写过 JS/TS,你大概率用过这种“按字符串取值/调用”的动态写法:
obj[propName]:按名字读取属性obj[propName] = v:按名字写入属性obj[methodName]():按名字调用方法
Java 反射的核心也类似:在运行时拿到“类的说明书(Class 对象)”,然后按名字去找字段/构造器/方法并操作。区别在于 Java 有更强的封装与权限控制,很多东西默认不能随便访问,所以会出现 setAccessible(true) 这种“打开后门”的操作。
使用反射的优缺点
优点
- 在 程序运行过程中 可以操作类对象,增加了程序的灵活性;
- 解耦:配合配置文件/约定,可以“换实现不改代码”,扩展性更强;
- 已知类名(字符串)也能创建对象、调用方法(常见于框架/插件化)。
缺点
- 性能问题:反射有额外开销,且很多优化手段用不上,一般比直接调用慢;
- 类型安全变差:大量
Object、强转、字符串写错要到运行时才爆(像 TS 里你用any的感觉); - 可维护性/安全性:
setAccessible(true)可能绕过封装,容易引入难排查问题;在有安全管理器/受限环境下也可能被禁止。
Class 对象的获取及使用
获取 Class 对象的方式
Class.forName("全类名")
通过“全类名字符串”拿到 Class。常见于 配置文件:把类名写在配置里,运行时读取再加载。
类名.class
通过字面量拿到 Class,最直观,常用在 参数传递(比如你要告诉别人“我要操作哪个类”)。
对象.getClass()
从一个已经存在的对象上拿到它的 Class(运行时真实类型)。
我们先定义一个 Person 类,用于后续反射示例:
package com.tomiaa;
public class Person {
private int age;
private String name;
public long id;
public long grade;
protected float score;
protected int rank;
public Person(int age, String name, long id, long grade, float score, int rank) {
this.age = age;
this.name = name;
this.id = id;
this.grade = grade;
this.score = score;
this.rank = rank;
}
public Person() {
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getGrade() {
return grade;
}
public void setGrade(long grade) {
this.grade = grade;
}
public float getScore() {
return score;
}
public void setScore(float score) {
this.score = score;
}
public int getRank() {
return rank;
}
public void setRank(int rank) {
this.rank = rank;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Person{");
sb.append("age=").append(age);
sb.append(", name='").append(name).append('\'');
sb.append(", id=").append(id);
sb.append(", grade=").append(grade);
sb.append(", score=").append(score);
sb.append(", rank=").append(rank);
sb.append('}');
return sb.toString();
}
}定义好 Person 类之后,我们尝试用 3 种不同的方式来获取 Class 对象,并比较它们是否相同。
package com.tomiaa;
public class Demo1 {
public static void main(String[] args) throws ClassNotFoundException {
// 第一种方式,Class.forName("全类名")
Class<?> class1 = Class.forName("com.tomiaa.Person");
System.out.println(class1);
// 第二种方式,类名.class
Class<?> class2 = Person.class;
System.out.println(class2);
// 第三种方式,对象.getClass()
Person person = new Person();
Class<?> class3 = person.getClass();
System.out.println(class3);
// 比较三个对象是否相同
System.out.println(class1 == class2);
System.out.println(class1 == class3);
}
}上述代码中,会发现最后输出的比较结果返回的是两个 true,说明通过上述三种方式获取的 Class 对象都是同一个,同一个字节码文件(*.class)在一次运行过程中只会被加载一次。
Class 对象的使用
获取成员变量
| 方法 | 说明 |
|---|---|
Field[] getFields() | 获取 public 字段(包含父类继承的 public 字段) |
Field getField(String name) | 获取某个 public 字段(包含父类继承的 public 字段) |
Field[] getDeclaredFields() | 获取 本类声明的所有字段(含 private/protected/default/public,不含父类) |
Field getDeclaredField(String name) | 获取某个 本类声明字段(不含父类) |
你可以把它理解成两条轴:
public vs declared:只要 public(
get*)还是“本类声明的全部”(getDeclared*)是否包含父类成员:
get*会包含继承来的 public 字段;getDeclared*不包含父类Field[] getFields()
package com.tomiaa;
import java.lang.reflect.Field;
public class Demo2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> class1 = Class.forName("com.tomiaa.Person");
Field[] fields = class1.getFields();
for (Field field : fields) {
System.out.println(field);
}
}
}回顾下我们的 Person 类,可以发现 id、grade 成员变量都是被 public 所修饰的,说明该方法是用于获取类中所有被 public 所修饰的成员变量(包括父类)。
Field getField(String name)
package com.tomiaa;
import java.lang.reflect.Field;
public class Demo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class<?> class1 = Class.forName("com.tomiaa.Person");
Field field1 = class1.getField("id");
System.out.println(field1);
Field field2 = class1.getField("grade");
System.out.println(field2);
// 注意:getField 只能拿到 public 字段
// 例如 age 是 private、rank 是 protected,直接 getField("age"/"rank") 会抛 NoSuchFieldException
// 要访问它们,请用 getDeclaredField(...) + setAccessible(true)
}
}从上面的结果分析可知,该方法只能用于获取类中指定名称的 public 所修饰的成员变量,对于 protected、private 所修饰的成员变量,该方法是无法获取的(包括父类)。而获取或设置成员变量值时,可以通过 get/set 方法来操作,具体操作方法如下。
// 假设我们获取到的 Field 为上面的 id,获取和设置 id 的值就可以通过如下操作来进行
// 1. 获取
Field idField = personClass.getField("id");
Person person = new Person();
Object idValue = idField.get(person);
System.out.println("id:" + idValue);
// 2. 设置
idField.set(person, 1312120L);
System.out.println("person:" + person);Field[] getDeclaredFields()
package com.tomiaa;
import java.lang.reflect.Field;
public class Demo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class<?> class1 = Class.forName("com.tomiaa.Person");
Field[] fields = class1.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
}
}观察上面的结果可知,该方法可用于获取所有的成员变量,不用考虑修饰符的限制(不包括父类)。
Field getDeclaredField(String name)
package com.tomiaa;
import java.lang.reflect.Field;
public class Demo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class<?> class1 = Class.forName("com.tomiaa.Person");
Field field1 = class1.getDeclaredField("id");
System.out.println(field1);
Field field3 = class1.getDeclaredField("rank");
System.out.println(field3);
Field field2 = class1.getDeclaredField("age");
System.out.println(field2);
}
}观察上面的结果可知,该方法可用于获取指定的成员变量,不用考虑成员变量修饰符的限制(不包括父类)。
但是:如果你要 get/set 的字段是 private/protected,需要先:
field.setAccessible(true);来绕过访问权限检查,否则会报 IllegalAccessException(这就是前面说的“打开后门”,用完要小心)。
获取构造方法
| 方法 | 说明 |
|---|---|
Constructor<?>[] getConstructors() | 获取 public 构造方法(仅当前类;构造方法不参与继承) |
Constructor<T> getConstructor(类<?>... parameterTypes) | 获取某个 public 构造方法(按参数类型匹配) |
Constructor<?>[] getDeclaredConstructors() | 获取 本类声明的所有构造方法(含 private/protected/default/public) |
Constructor<T> getDeclaredConstructor(类<?>... parameterTypes) | 获取某个 本类声明构造方法(按参数类型匹配) |
package com.tomiaa;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Demo3 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
Class<?> personClass = Class.forName("com.tomiaa.Person");
// 1. 获取所有构造方法
System.out.println("所有构造方法");
Constructor[] constructors = personClass.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}
// 2. 获取指定构造方法
// 空参构造方法
System.out.println("空参构造方法");
Constructor constructor1 = personClass.getConstructor();
System.out.println(constructor1);
// 带参构造方法
System.out.println("带参构造方法");
Constructor constructor2 = personClass.getConstructor(int.class, String.class, long.class, long.class, float.class,
int.class);
System.out.println(constructor2);
// 获取构造方法后,可以利用它来创建对象
System.out.println("空参创建对象");
// 第一种方法
Object person = constructor1.newInstance();
System.out.println(person);
// 第二种方法
// 注意:Class#newInstance() 在较新版本里不推荐使用(异常处理不友好)
// 更推荐:personClass.getDeclaredConstructor().newInstance()
Object person1 = personClass.getDeclaredConstructor().newInstance();
System.out.println(person1);
System.out.println("带参创建对象");
Object object = constructor2.newInstance(20, "村雨遥", 1312020, 3, 99.0F, 2);
System.out.println(object);
}
}Constructor<?>[] getConstructors()获取当前类的所有
public构造方法。Constructor<T> getConstructor(类<?>... parameterTypes)
获取某一个指定参数类型的 public 构造方法。
Constructor<?>[] getDeclaredConstructors()
获取当前类 声明的所有构造方法(不管修饰符是什么)。
Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
获取某一个指定参数类型的“本类声明构造方法”(不管修饰符是什么)。
获取到构造方法之后,就可以用 newInstance(...) 来创建实例;如果你拿到的是 private 构造方法,同样需要 setAccessible(true)。
获取成员方法
| 方法 | 说明 |
|---|---|
Method[] getMethods() | 获取 public 方法(包含父类/接口继承来的 public 方法) |
Method getMethod(String name, 类<?>... parameterTypes) | 获取某个 public 方法(包含父类/接口继承来的 public 方法) |
Method[] getDeclaredMethods() | 获取 本类声明的所有方法(含 private/protected/default/public,不含继承来的) |
Method getDeclaredMethod(String name, 类<?>... parameterTypes) | 获取某个 本类声明方法(不含继承来的) |
package com.tomiaa;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Demo4 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> personClass = Class.forName("com.tomiaa.Person");
// 获取所有 public 成员方法
System.out.println("获取所有成员方法");
Method[] methods = personClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
// 获取指定名称的方法
System.out.println("获取指定名称的方法");
Method getAgeMethod = personClass.getMethod("getAge");
System.out.println(getAgeMethod);
// 执行方法
Person person = new Person(20, "村雨遥", 1312020, 3, 99.0F, 2);
// invoke(目标对象, 参数1, 参数2, ...)
int age = (int) getAgeMethod.invoke(person);
System.out.println(age);
}
}补充说明:
- 想“把所有能在外面调用的 public 方法都列出来(含继承)”→
getMethods() - 想“把这个类自己写的所有方法都列出来(含 private)”→
getDeclaredMethods() - 方法执行统一靠
invoke(),这点和前端的fn.apply(obj, args)很像(只是 Java 还会有受检异常)。
获取类名
package com.tomiaa;
public class Demo5 {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Person();
Class<?> personClass = person.getClass();
String className = personClass.getName();
System.out.println(className);
}
}String getName()
从上述程序的结果可知,当我们获取到 Class 对象之后,如果不知道类的全名,就可以使用 getName() 来获取该类的全名。
反射实例
假设我们有如下需求:在不改变类的代码的前提下,我们能够创建任意类的对象,并执行其中的方法。
此时,我们可以通过 配置文件 + 反射 来实现。用前端类比就是:
- 你把“要用哪个模块/组件、要调用哪个方法”写在配置里(像路由表/插件注册表)
- 程序启动时读取配置并按需加载/调用
很多框架的“约定 + 自动装配/插件化”的底层,都能看到这个思路的影子。
假设我们有两个类,一个 Student,一个 Teacher,两者的定义如下;
package com.tomiaa;
public class Teacher {
private String name;
private int age;
public void teach() {
System.out.println("教书育人……");
}
}package com.tomiaa;
public class Student {
private String name;
private float score;
public void study() {
System.out.println("好好学习,天天向上……");
}
}要实现我们的需求,通常需要如下步骤:
- 将要创建对象的全类名和要执行的方法都配置在配置文件中;
定义配置文件 prop.properties,用两个 key 表示“要创建哪个类 / 要调用哪个方法”:
className=com.tomiaa.Student
methodName=study- 然后在主方法中加载读取配置文件;
// 创建配置对象
Properties properties = new Properties();
// 加载配置文件(真实项目里建议做空值检查、并用 try-with-resources 关闭流)
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("prop.properties");
properties.load(inputStream);
// 读配置:类名 + 方法名
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");- 利用反射技术将类加载到内存中;
// 加载进内存
Class<?> name = Class.forName(className);- 接着创建对象(更推荐走构造器的
newInstance());
// 创建实例
Object object = name.getDeclaredConstructor().newInstance();- 最后则是利用
invoke()方法来执行方法;
// 获取并执行方法
Method method = name.getMethod(methodName);
method.invoke(object);将整个流程汇总起来就是:
package com.tomiaa;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
public class ReflectTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
// 创建配置文件对象
Properties properties = new Properties();
// 加载配置文件
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("prop.properties");
properties.load(inputStream);
// 获取配置文件中定义的数据
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");
// 加载进内存
Class<?> name = Class.forName(className);
// 创建实例
Object object = name.getDeclaredConstructor().newInstance();
// 获取并执行方法
Method method = name.getMethod(methodName);
method.invoke(object);
}
}此时,我们只需要改动配置文件 prop.properties 中的配置即可输出不同结果;
小结(给前端的记忆点)
- 反射 = 运行时 根据“字符串名字”去拿字段/构造器/方法并操作
get*往往更“外部视角”(public + 可能含继承);getDeclared*更“本类视角”(含 private,但不含继承)- 能不用反射就别用:优先正常调用;必须用时,尽量把反射集中在框架层/工具层,别散落业务代码里