首页 / Java 入门教程 / 反射应用(创建对象、读写字段、调用方法)

Java 入门教程

反射应用(创建对象、读写字段、调用方法)

本教程共 100 篇 · 第 53 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程反射ReflectionMethodField框架原理

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 为例,它的核心套路就是:

  1. 扫描所有类,找到贴了 @Component(及其衍生注解)的类;
  2. Class.forName() 把它们的 Class 对象拿到;
  3. 用构造方法 newInstance() 创建实例,放进容器管理;
  4. 再扫描字段/方法上的 @Autowired 等注解,用 Field.set / Method.invoke 把依赖「注」进去。

你写 @Component 只是贴了个标签;真正「按标签创建并装配对象」的,全靠反射。注解负责「声明意图」,反射负责「落实动作」——他俩是固定搭档。

Note

反射也有代价:运行期解析类结构比直接调用略慢,且绕过了编译期类型检查(写错方法名运行期才炸)。所以业务代码里别滥用,把反射交给框架和工具去做最恰当。

反射代码里常见的异常

写反射时这几个异常最高频,提前心里有数:

  • ClassNotFoundExceptionClass.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 基线)的武器库。