前言
刚学 Java 的小伙伴(包括我在内)可能会有疑问:“我平时 new 对象、调用方法都好好的,为啥要学反射?反射到底是干啥的?” 其实反射是 Java 里一个 “很灵活但不难懂” 的特性,今天就用大白话 + 实例,彻底搞懂反射的作用和用法。
一、先搞懂:反射到底是什么?
我们先从 “平时怎么用类” 说起。比如我们写了一个User类,平时用它的时候,是这样的:
// 1. 按“类的说明书”创建对象(new关键字)
User user = new User();
// 2. 按“说明书”调用方法/操作属性
user.setName("张三");
System.out.println(user.getName());
这就像我们拿到一部手机(User类),手里有说明书(类的源码 / API 文档),我们按说明书操作:开机(new 对象)、点微信(调用方法)—— 这是 “正向操作”:先知道类的结构,再用它。
而反射呢?就是 “没有说明书,直接拆手机看内部结构,还能操作零件”。换句话说:
反射允许我们在程序 “运行的时候”,不用提前知道类的结构,就能获取类的属性、方法、构造器,还能创建对象、调用方法 —— 哪怕是私有属性和方法!
举个生活场景:你用 Spring 框架时,配置文件里写个com.test.User,Spring 就能帮你创建User对象。Spring 怎么做到的?它根本不知道你写的User类长啥样,靠的就是反射 —— 运行时读取配置里的类名,再通过反射创建对象。
二、为什么需要反射?
小伙伴们可能会问:“我直接 new 对象不香吗?为啥要费劲儿用反射?” 其实反射在很多场景下是 “不得不⽤”,比如这 3 个常见场景:
场景 1:框架开发(最常用)
像 Spring、MyBatis 这些框架,核心就是反射。比如:
- Spring 的 IOC 容器:你在配置文件或注解里写
@Component("user"),Spring 在运行时通过反射找到这个类,创建对象并存起来,你用的时候直接拿,不用自己 new; - MyBatis 的 Mapper:你写个
UserMapper接口,MyBatis 在运行时通过反射 “动态生成接口的实现类”,帮你执行 SQL,不用你自己写实现。
没有反射,这些框架根本没法实现 —— 因为框架开发者根本不知道你会定义哪些类、哪些接口。
场景 2:动态操作类(比如 “不知道类名,只知道字符串”)
假设你做一个 “插件系统”,用户在配置文件里写要加载的插件类名(比如com.plugin.LogPlugin),你的程序运行时才知道要加载这个类。这时候没法用new(因为你写代码时根本不知道LogPlugin这个类),只能用反射:
// 从配置文件读来的类名字符串
String className = "com.plugin.LogPlugin";
// 用反射创建对象
Class<?> clazz = Class.forName(className);
Object plugin = clazz.newInstance();
// 用反射调用插件的方法
Method method = clazz.getMethod("start");
method.invoke(plugin);
场景 3:操作私有成员(比如 “修改别人写的类的私有属性”)
有时候你用别人写的 jar 包,里面的类有个私有属性private String name,没有 setter 方法,但你就是要改它的值。这时候反射就能 “突破封装”,直接操作私有属性(注意:实际开发中慎用,会破坏类的封装性)。
三、反射怎么用?小白也能上手的 4 步走
反射的核心是 “Class对象”—— 它是反射的 “入口”,代表了类的 “元信息”(比如类的名字、属性、方法、构造器)。所有反射操作,都要先拿到Class对象。
我们先定义一个简单的User类,后面的例子都用它:
// 简单的User类,有属性、构造器、方法(包含私有)
public class User {
// 公有属性
public String name;
// 私有属性
private int age;
// 无参构造器
public User() {}
// 有参构造器
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 公有方法
public void sayHello() {
System.out.println("Hello, 我是" + name);
}
// 私有方法
private void showAge() {
System.out.println("我的年龄是" + age);
}
// getter/setter(为了对比反射操作,先写正常的setter)
public void setAge(int age) {
this.age = age;
}
}
第一步:获取 Class 对象(反射的入口)
有 3 种常用方式获取Class对象,记住这 3 种就行,不用纠结底层:
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| 1. 对象.getClass () | User user = new User(); Class<?> clazz = user.getClass(); | 已经创建了对象,想获取它的类信息 |
| 2. 类名.class | Class<?> clazz = User.class; | 知道类名,还没创建对象(编译时已知类) |
| 3. Class.forName() | Class<?> clazz = Class.forName("com.test.User"); | 只知道类的全路径字符串(编译时未知类,比如从配置文件读的) |
注意:一个类在 JVM 中只有一个Class对象,不管用哪种方式获取,拿到的都是同一个。
第二步:通过 Class 对象创建实例(代替 new)
拿到Class对象后,有 2 种方式创建类的实例(相当于new User()):
方式 1:调用无参构造器(常用)
// 1. 获取Class对象(这里用第3种方式,模拟从配置读类名)
Class<?> clazz = Class.forName("com.test.User");
// 2. 调用无参构造器创建实例(相当于new User())
Object user = clazz.newInstance();
// 3. 验证:强转后调用方法(和正常对象一样用)
User u = (User) user;
u.setName("张三");
u.sayHello(); // 输出:Hello, 我是张三
方式 2:调用有参构造器(如果没有无参构造器)
如果类只有有参构造器(没有无参),就需要先获取有参构造器,再创建实例:
// 1. 获取Class对象
Class<?> clazz = User.class;
// 2. 获取有参构造器(参数是构造器的参数类型,这里是String和int)
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 3. 用有参构造器创建实例(传入构造器的参数值)
Object user = constructor.newInstance("李四", 20);
// 4. 验证
User u = (User) user;
u.sayHello(); // 输出:Hello, 我是李四
第三步:用反射调用方法(包括私有方法)
平时调用方法是user.sayHello(),反射调用方法分 3 步:
- 用
Class对象获取Method对象(指定方法名和参数类型); - (如果是私有方法)调用
method.setAccessible(true)突破封装; - 用
method.invoke(对象, 方法参数)调用方法。
示例 1:调用公有方法(sayHello ())
// 1. 先获取Class对象和实例
Class<?> clazz = User.class;
Object user = clazz.newInstance();
User u = (User) user;
u.setName("王五"); // 先给name赋值
// 2. 获取公有方法sayHello(无参数,所以参数类型传null)
Method sayHelloMethod = clazz.getMethod("sayHello", null);
// 3. 调用方法:invoke(实例对象, 方法参数)
sayHelloMethod.invoke(user, null); // 输出:Hello, 我是王五
示例 2:调用私有方法(showAge ())
私有方法需要多一步setAccessible(true),告诉 JVM “允许访问私有成员”:
// 1. 获取Class对象和实例(用有参构造器给age赋值22)
Class<?> clazz = User.class;
Object user = clazz.getConstructor(String.class, int.class).newInstance("赵六", 22);
// 2. 获取私有方法showAge(注意:getDeclaredMethod能获取私有方法,getMethod只能获取公有)
Method showAgeMethod = clazz.getDeclaredMethod("showAge", null);
// 3. 突破封装:允许访问私有方法
showAgeMethod.setAccessible(true);
// 4. 调用私有方法
showAgeMethod.invoke(user, null); // 输出:我的年龄是22
第四步:用反射操作属性(包括私有属性)
平时操作属性是user.name或user.setAge(),反射操作属性和调用方法类似:
- 用
Class对象获取Field对象(指定属性名); - (如果是私有属性)调用
field.setAccessible(true); - 用
field.get(对象)获取属性值,field.set(对象, 值)设置属性值。
示例:直接修改私有属性 age(不用 setter)
// 1. 获取Class对象和实例
Class<?> clazz = User.class;
Object user = clazz.newInstance();
// 2. 获取私有属性age(getDeclaredField获取私有属性,getField获取公有)
Field ageField = clazz.getDeclaredField("age");
// 3. 突破封装:允许访问私有属性
ageField.setAccessible(true);
// 4. 设置私有属性值:set(实例对象, 要设置的值)
ageField.set(user, 25);
// 5. 获取私有属性值:get(实例对象)
int age = (int) ageField.get(user);
System.out.println("反射获取的age:" + age); // 输出:25
四、反射的优缺点:
反射虽然灵活,但不是万能的,新手(包括小编)要知道它的 “坑” 在哪里:
优点:
- 灵活性高:运行时动态操作类,不用提前知道类结构(框架的核心);
- 突破封装:能操作私有属性和方法(特殊场景有用,但要慎用);
- 解耦:比如框架和业务类分离,框架不用依赖具体的业务类(比如 Spring 不用知道你的 User 类)。
缺点:
- 性能差:反射是 “运行时解析”,比直接 new 对象、调用方法慢(因为 JVM 没法提前优化);
- 破坏封装:访问私有成员会打破类的封装性,可能导致代码不安全、难维护;
- 可读性差:反射代码比正常代码复杂,新手可能看不懂(比如一堆 Method、Field 对象)。
新手建议:平时开发中,能不用反射就不用(比如能 new 对象就别用反射创建);只有在必须动态操作类的时候才用(比如写框架、插件系统)。
五、总结:新手入门反射的 3 个重点
- 核心入口是 Class 对象:所有反射操作都要先拿到 Class 对象,3 种获取方式要记牢;
- 反射的本质是 “运行时操作”:不用提前知道类结构,能动态创建对象、调用方法、操作属性;
- 别滥用反射:性能差且破坏封装,日常开发优先用正常方式(new、调用方法),框架或特殊场景再用反射。
如果刚开始觉得反射有点绕,没关系 —— 先把 “获取 Class 对象→创建实例→调用方法→操作属性” 这四步的代码抄下来跑一遍,改改属性名、方法名,慢慢就懂了。

523

被折叠的 条评论
为什么被折叠?



