反射应用(创建对象、读写字段、调用方法)
本教程共 100 篇 · 第 53 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
53. 反射应用(创建对象、读写字段、调用方法)
本节目标:把上一章的 Class 对象真正用起来——运行期创建对象、读写字段、调用方法,并看懂框架「反射 + 注解」自动装配的底层原理。
准备一个被操作的类
为了演示,先写一个普通类,字段和方法都有公开和私有的:
public class Target {
private String value = "初始值";
public Target() { } // 无参构造
public void hello(String name) {
System.out.println("你好," + name);
}
private void secret() {
System.out.println("私有方法,value=" + value);
}
}
```bash
下面所有操作都通过反射来做,而不是 `new Target()` 后直接调。
## 动态创建对象
拿到 `Class` 后,选出构造方法,再 `newInstance()`:
```java
import java.lang.reflect.*;
Class<?> cls = Class.forName("Target"); // 全类名
Object obj = cls.getDeclaredConstructor().newInstance(); // 调无参构造创建实例
getDeclaredConstructor() 取无参构造,newInstance() 真正创建对象。整个过程里我们没有写 new Target(),类名是从字符串来的——这就是框架「按配置造对象」的核心。
Note早期 JDK 用
cls.newInstance(),但它在构造方法抛异常时会把检查异常吞掉,已不推荐使用。Java 9 起标准做法是用getDeclaredConstructor().newInstance()。教程主线统一用后者。
调用方法
用 getDeclaredMethod("方法名", 参数类型...) 拿到 Method,再用 invoke(对象, 实参...) 调用:
Method m = cls.getDeclaredMethod("hello", String.class);
m.invoke(obj, "小明"); // 输出:你好,小明
```text
对于**私有方法**,要先用 `setAccessible(true)` 关掉访问检查,否则 `invoke` 会抛 `IllegalAccessException`:
```java
Method secret = cls.getDeclaredMethod("secret");
secret.setAccessible(true); // 解除私有限制
secret.invoke(obj); // 输出:私有方法,value=初始值
Warning
setAccessible(true)能越过private检查,属于「打破封装」的手段。它只应在自己完全掌控的代码里做演示或特殊工具(如序列化、测试框架)中使用,生产业务代码滥用会破坏封装、带来安全隐患。模块化(JPMS)下还可能被模块层拒绝。
读写字段
用 getDeclaredField("字段名") 拿到 Field,再用 get/set 读写:
Field f = cls.getDeclaredField("value");
f.setAccessible(true); // value 是 private,需放开
System.out.println(f.get(obj)); // 读:初始值
f.set(obj, "新值"); // 写
System.out.println(f.get(obj)); // 读:新值
```bash
字段读写同样区分 `getField`(仅 public)/ `getDeclaredField`(本类所有),私有字段记得 `setAccessible(true)`。
## 完整可运行示例
```java
import java.lang.reflect.*;
public class Target {
private String value = "初始值";
public void hello(String name) { System.out.println("你好," + name); }
private void secret() { System.out.println("私有方法,value=" + value); }
}
public class Main {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("Target");
Object obj = cls.getDeclaredConstructor().newInstance();
// 调公开方法
Method m = cls.getDeclaredMethod("hello", String.class);
m.invoke(obj, "小明");
// 调私有方法
Method s = cls.getDeclaredMethod("secret");
s.setAccessible(true);
s.invoke(obj);
// 读写私有字段
Field f = cls.getDeclaredField("value");
f.setAccessible(true);
System.out.println("改之前:" + f.get(obj));
f.set(obj, "被反射改了");
System.out.println("改之后:" + f.get(obj));
}
}
输出:
你好,小明
私有方法,value=初始值
改之前:初始值
改之后:被反射改了
```bash
## 获取继承关系
上一章提过,这里再补全几个常用 API:
```java
Class<?> cls = Target.class;
cls.getSuperclass(); // 直接父类 Class
cls.getInterfaces(); // 实现的接口数组
// 判断 cls 是不是某个类/接口的自己或子类
Number.class.isAssignableFrom(Integer.class); // true
框架常拿 isAssignableFrom 判断「这个类有没有实现我关心的接口」,再决定要不要对它做处理。
框架到底用反射做了什么
理解了上面这些,很多「黑魔法」就不神秘了。以 Spring 为例,它的核心套路就是:
- 扫描所有类,找到贴了
@Component(及其衍生注解)的类; - 用
Class.forName()把它们的Class对象拿到; - 用构造方法
newInstance()创建实例,放进容器管理; - 再扫描字段/方法上的
@Autowired等注解,用Field.set/Method.invoke把依赖「注」进去。
你写 @Component 只是贴了个标签;真正「按标签创建并装配对象」的,全靠反射。注解负责「声明意图」,反射负责「落实动作」——他俩是固定搭档。
Note反射也有代价:运行期解析类结构比直接调用略慢,且绕过了编译期类型检查(写错方法名运行期才炸)。所以业务代码里别滥用,把反射交给框架和工具去做最恰当。
反射代码里常见的异常
写反射时这几个异常最高频,提前心里有数:
ClassNotFoundException:Class.forName的类名写错,或类不在 classpath;NoSuchMethodException/NoSuchFieldException:方法名、字段名或参数类型写错;IllegalAccessException:没setAccessible(true)就去碰非 public 成员;InvocationTargetException:被调用的方法内部抛了异常,它会被包在这层外面,用getCause()拿到真正的异常。
NoSuchMethodException 最容易在参数类型上翻车。方法签名是 void set(int n),你按 Integer.class 去找就找不到:
cls.getDeclaredMethod("set", Integer.class); // 找不到
```text
```text
Exception in thread "main" java.lang.NoSuchMethodException: Target.set(java.lang.Integer)
at java.base/java.lang.Class.getDeclaredMethod(Class.java:2675)
改成 int.class 才对——基本类型和它的包装类在反射里是两个不同的 Class。方法名末尾多个空格、把无参构造写成 getDeclaredConstructor(null),也都是同一类低级错误的常见来源。
setAccessible 在模块系统下会被拦
Java 9 引入模块系统(JPMS)之后,setAccessible(true) 不再是万能钥匙。改自己写的类照样有效,去掰 JDK 内部实现就会被挡回来:
Field f = String.class.getDeclaredField("value");
f.setAccessible(true); // 运行期直接抛异常
```java
```text
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make field private final byte[] java.lang.String.value accessible:
module java.base does not "opens java.lang" to unnamed module @1b6d3586
看最后一句就懂了:java.base 模块没把 java.lang 包「开放(opens)」给你的代码。想临时放行,得在启动命令上开口子:
java --add-opens java.base/java.lang=ALL-UNNAMED Main
```bash
> [!WARNING]
> `--add-opens` 是应急手段,不是长期方案。它把 JDK 内部实现暴露给你,下个版本内部字段一改名,程序就崩。业务代码请老老实实走公开 API。
## 反射要付的三笔账
**慢**:每次 `invoke` 都要做访问检查、参数装箱和方法查找,比直接调用慢一个量级。JIT 预热后差距会缩小,但循环几百万次仍然看得出来。补救办法是把 `Method`、`Field` 对象缓存成静态字段复用,别在循环里反复 `getDeclaredMethod`。
**不安全**:`setAccessible(true)` 等于把封装的门拆了,别人的私有状态随便读写。工具库这么干可以,业务代码这么干就是埋雷。
**编译器帮不上忙**:方法名藏在字符串里,IDE 重命名重构时不会跟着改,只有运行到那一行才暴露。所以反射代码的测试覆盖要比普通代码更严。
## 给初学者的使用建议
反射很强,但别滥用:
- 业务代码尽量直接写、直接调,类型安全、可读性好;
- 把反射留给框架、工具库、测试、序列化等「必须在运行期才知道类型」的场景;
- 一旦用反射,名字、参数类型宁可写常量、写测试,别硬编码字符串到处飞。
理解了这些,以后再看到 Spring、MyBatis、JUnit 这些框架「自动」帮你做事,你就能在脑子里还原出「扫描注解 + 反射创建与调用」的完整链路了。
## 没有无参构造怎么办
前面演示的都是调无参构造。如果目标类只有带参构造,就用 `getDeclaredConstructor(参数类型...)` 指定期望的构造方法,再在 `newInstance(实参...)` 里传对应的值,思路完全一致:
```java
Constructor<?> c = cls.getDeclaredConstructor(String.class, int.class);
Object obj = c.newInstance("小明", 18);
Tip反射创建对象的本质就是「选一个构造方法 + 传实参」,无论无参还是带参,套路都一样。
小结
- 用
getDeclaredConstructor().newInstance()在运行期创建对象(替代旧式newInstance())。 - 用
Method.invoke(obj, 实参)调用方法,私有方法需先setAccessible(true)。 - 用
Field.get/set读写字段,私有字段同样需setAccessible(true)。 getSuperclass()/getInterfaces()/isAssignableFrom()用于分析继承关系。- 框架(如 Spring)的本质:扫描注解得到
Class→ 反射创建实例 → 反射注入依赖。注解声明意图,反射落实动作。
到此,面向对象与语言特性的核心章节告一段落。你已经掌握了从继承、多态,到接口、记录类、密封类、枚举、注解与反射这一整套现代 Java(Java 25 LTS 基线)的武器库。