【JAVASE | 第十八篇】Java 反射

前言

反射允许程序在运行时分析和操作类的组成部分。平时写代码时,我们一般直接 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() 获取这个类声明的所有字段。

循环中的三步是核心:

  1. 通过 field.getName() 获取字段名。
  2. 通过 field.setAccessible(true) 允许访问私有字段。
  3. 通过 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,最后创建对象、读写字段或调用方法。

真正需要记住的是它的使用边界。反射能突破封装,也能绕过泛型约束,所以它适合写工具和框架,不适合让普通业务代码到处依赖它。判断要不要用反射,可以抓住一个问题:当前代码是不是在运行时才知道要处理哪个类。如果答案是肯定的,反射就有用;如果类型早就明确,直接调用往往更稳。

参考资料

相关推荐
源码宝1 小时前
智能随访系统源码,技术架构设计:Spring Boot + Vue.js + 微服务实战
java·人工智能·源码·随访系统·智能随访·随访系统成品源码
zyl837211 小时前
Java 后端完整技术栈
java·开发语言
想带你从多云到转晴1 小时前
04、JAVAEE---多线程进阶、文件I/O、网络初识
java·java-ee
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第107题】【并发篇】第7题:说说 Lock 锁?
java·开发语言·面试
杨了个杨89822 小时前
Dockerfile介绍及镜像制作
java·开发语言
c++之路2 小时前
CMake 系列教程(三):变量、条件与控制流
java·windows·spring
一条泥憨鱼2 小时前
苍穹外卖【day5|Redis与店铺营业状态设置】
java·后端·mybatis·苍穹外卖
要开心吖ZSH2 小时前
AI医疗分诊与健康咨询助手agent开发——(2)让AI输出可控:结构化分诊与安全规则
java·ai·agent·健康医疗·spring ai
San813_LDD4 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript