前言
反射允许程序在运行时分析和操作类的组成部分。平时写代码时,我们一般直接 new 对象、调用方法、访问 getter/setter;使用反射时,程序可以先拿到一个类的 Class 对象,再继续获取构造器、成员变量和成员方法。
这篇文章结合 day06-junit-reflect-annotation-proxy 中的代码,把反射按一条线讲清楚:先拿到类,再拿到类的组成部分,最后用这些组成部分完成一个通用的对象保存工具。
一、反射到底操作什么
一个类在反射里常见的组成部分可以对应成下面几类对象。
| 类的成分 | 反射中的类型 | 常见用途 |
|---|---|---|
| 构造器 | Constructor | 创建对象 |
| 成员变量 | Field | 读取或修改字段值 |
| 成员方法 | Method | 调用方法 |
这些对象都来自同一个入口:Class 对象。
在普通代码里,我们写的是:
java
Dog dog = new Dog("小白", 5);
dog.setAge(6);
dog.getAge();
在反射里,程序更关心的是:这个对象属于哪个类?这个类有哪些字段?字段叫什么?字段值是多少?方法是否能被调用?所以它适合做框架、工具、注解解析、序列化这类"提前不知道具体类型"的场景。
二、获取 Class 对象的三种方式
项目里的 ReflectDemo1 展示了反射的第一步:获取类本身。
java
Class c1 = Stu.class;
Class c2 = Class.forName("itheima.demo2reflect.Stu");
Stu s = new Stu();
Class c3 = s.getClass();
三种写法拿到的都是 Stu 对应的类信息,只是使用场景不同。
类名.class 适合编译期已经明确知道类型的情况,写法最直接。
Class.forName(...) 适合类名来自配置文件或字符串的情况。很多框架加载驱动类、扫描类时都会用到这种思路。
对象.getClass() 适合手里已经有对象,但不知道它具体属于哪个类的情况。后面的通用保存工具就是这种场景。
拿到 Class 以后,可以继续读取类名。
java
Class c1 = Stu.class;
System.out.println(c1.getName());
System.out.println(c1.getSimpleName());
getName() 会得到完整类名,包含包名;getSimpleName() 只得到简单类名,例如 Stu。
三、获取构造器并创建对象
Dog 类里有三个构造器,其中无参构造器是私有的。
java
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
private Dog() {}
public Dog(String name) {
this.name = name;
}
在 ReflectDemo2 中,可以通过 getDeclaredConstructors() 获取当前类中声明的全部构造器。
java
Class c2 = Dog.class;
Constructor[] cons = c2.getDeclaredConstructors();
for (Constructor con : cons) {
System.out.println(con.getName() + " " + con.getParameterCount());
}
这里的 getParameterCount() 用来查看构造器参数个数。判断重载构造器时,参数列表非常关键。
如果只想拿某一个构造器,可以指定参数类型。
java
Constructor con = c2.getDeclaredConstructor();
Constructor con2 = c2.getDeclaredConstructor(String.class, int.class);
第一行获取无参构造器。第二行获取参数为 String 和 int 的构造器。
由于 Dog() 是私有构造器,正常情况下不能在类外直接调用。反射可以通过下面这一步关闭普通访问检查。
java
con.setAccessible(true);
Dog d1 = (Dog) con.newInstance();
官方 Java API 中,Constructor.newInstance(Object...) 的作用就是使用这个构造器创建并初始化对象。现在更推荐通过构造器对象调用 newInstance(),而不是直接使用已经过时的 Class.newInstance()。
这里能看出反射的第一个特点:它可以绕过一部分封装限制。但这也意味着代码安全性和可读性都会下降,不能把它当成日常业务代码的首选写法。
四、获取成员变量并读写字段
Dog 中有两个私有字段。
java
private String name;
private int age;
反射可以拿到它们。
java
Class c1 = Dog.class;
Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName() + " " + field.getType().getName());
}
getName() 得到字段名,getType().getName() 得到字段类型。这里不会调用 getter,而是直接分析类的字段结构。
如果要获取单个字段,可以按字段名查找。
java
Field field = c1.getDeclaredField("name");
Field field2 = c1.getDeclaredField("age");
拿到字段对象以后,字段的核心操作就是取值和赋值。
java
Dog dog = new Dog("小红");
field2.setAccessible(true);
field2.setInt(dog, 18);
int age = field2.getInt(dog);
System.out.println(age);
setInt(dog, 18) 表示把 dog 这个对象的 age 字段设置成 18。getInt(dog) 表示从同一个对象中读取 age 的值。
这里容易误解的一点是:Field 代表的是字段本身,不代表某个对象里的具体值。只有把目标对象传进去,反射才知道要操作谁的字段。
五、获取成员方法并调用
方法也可以通过反射拿到。
java
Class c1 = Dog.class;
Method[] methods = c1.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName() + " " + method.getReturnType().getName());
}
如果只获取某一个方法,也要写清楚方法名和参数类型。
java
Method method = c1.getDeclaredMethod("eat");
Method method2 = c1.getDeclaredMethod("getName", String.class);
项目中的 eat() 是私有方法。
java
private void eat() {
System.out.println("狗吃shift");
}
调用私有方法时,同样需要先打开访问权限。
java
Dog dog = new Dog("小花", 7);
method.setAccessible(true);
method.invoke(dog);
invoke(dog) 的意思是调用 dog 对象上的这个方法。方法如果有参数,就要继续把参数传进去。
反射调用方法时,最容易卡在两个地方:方法名写错,或者参数类型没对上。尤其是重载方法,方法名一样,参数列表不同,反射查找时必须把参数类型写准确。
六、反射的实际用途:保存任意对象
如果只看前面的 API,反射很容易变成"方法清单"。项目里的 SaveObjectFraeWork 更能说明它为什么有用。
这个工具方法接收的是 Object。
java
public static void saveObject(Object obj) throws Exception {
PrintStream ps = new PrintStream(
new FileOutputStream("D:\\Java\\code\\javaseprojectmax\\day06-junit-reflect-annotation-proxy\\src\\output.txt", true)
);
Class a = obj.getClass();
String name = a.getSimpleName();
ps.println("=============" + name + "================");
Field[] fields = a.getDeclaredFields();
for (Field field : fields) {
String fieldname = field.getName();
field.setAccessible(true);
Object fieldValue = field.get(obj) + "";
ps.println(fieldname + "========" + fieldValue);
}
ps.close();
}
这个方法不关心传进来的是 Dog、Stu 还是 Teacher。它先通过 obj.getClass() 获取真实类型,再通过 getDeclaredFields() 获取这个类声明的所有字段。
循环中的三步是核心:
- 通过 field.getName() 获取字段名。
- 通过 field.setAccessible(true) 允许访问私有字段。
- 通过 field.get(obj) 获取当前对象中这个字段的值。
调用代码如下。
java
Dog dog = new Dog("小白", 5);
SaveObjectFraeWork.saveObject(dog);
Stu stu = new Stu("小明", "Java", 100);
SaveObjectFraeWork.saveObject(stu);
Teacher teacher = new Teacher("小红", 30, "吃饭", 30000, "123");
SaveObjectFraeWork.saveObject(teacher);
保存后的文件内容大致会按类名和字段展开。
text
=============Dog================
name========小白
age========5
=============Stu================
name========小明
hobby========Java
score========100
=============Teacher================
name========小红
age========30
hobby========吃饭
salary========30000.0
className========123
这就是反射最典型的价值:当方法参数只是一个普通 Object 时,程序仍然可以在运行时知道它有哪些字段,并把字段名和值统一处理掉。
很多框架的底层思想也和这个类似。比如对象转 JSON、ORM 框架映射字段、注解解析、测试工具执行方法,都会用到"运行时分析类结构"的能力。
七、绕过泛型约束要谨慎
笔记里提到反射可以绕过泛型约束。原因是 Java 泛型主要在编译期做类型检查,运行时集合的 add 方法本质上接收的是 Object。
示例思路如下。
java
ArrayList<String> list = new ArrayList<>();
list.add("小明");
Class c1 = list.getClass();
Method add = c1.getDeclaredMethod("add", Object.class);
add.invoke(list, 100);
这段代码通过反射调用 ArrayList 的 add(Object) 方法,把整数放进了声明为 ArrayList 的集合中。
这里要注意 invoke 的参数。第一个参数是目标对象,也就是要往哪个集合里添加;第二个参数才是真正要添加的数据。如果写成 add.invoke(1),反射会把 1 当成目标对象,调用会失败。
这个例子能说明反射能力很强,但也说明它很危险。绕过泛型以后,后续代码如果按字符串处理集合元素,就可能在运行时出错。
八、常见问题和排查顺序
写反射代码时,可以按下面的顺序排查。
第一,先确认 Class 对象是否拿对。使用 Class.forName(...) 时,完整类名必须包含包名。
第二,查构造器、字段、方法时,区分 getXXX 和 getDeclaredXXX。前者更偏向公开成员,后者能拿到当前类声明的成员,包括私有成员。
第三,访问私有构造器、字段、方法前,要确认是否需要调用 setAccessible(true)。Java 官方 API 把 Field、Method、Constructor 都归到 AccessibleObject 体系下,它们都可以通过这个入口控制访问检查。
第四,调用方法或构造器时,参数类型要和声明保持一致。int.class 和 Integer.class、无参和有参、重载方法的不同参数列表,都可能影响查找结果。
第五,不要把反射当成普通业务代码的默认选择。能直接调用 getter、setter、构造器时,直接调用通常更清晰。只有在类型不确定、需要通用处理、框架扩展、工具封装时,反射才更合适。
总结
反射的主线并不复杂:先拿到 Class,再拿到 Constructor、Field、Method,最后创建对象、读写字段或调用方法。
真正需要记住的是它的使用边界。反射能突破封装,也能绕过泛型约束,所以它适合写工具和框架,不适合让普通业务代码到处依赖它。判断要不要用反射,可以抓住一个问题:当前代码是不是在运行时才知道要处理哪个类。如果答案是肯定的,反射就有用;如果类型早就明确,直接调用往往更稳。
参考资料
- Oracle Java SE 25 API:java.lang.Class
- Oracle Java SE 25 API:java.lang.reflect 包说明
- CSDN 参考文章:Java基础之---反射(非常重要)
- 项目代码:day06-junit-reflect-annotation-proxy/src/itheima/demo2reflect