摘要: 本文深入解析Java反射机制的核心原理与实践应用,从获取Class对象到动态访问字段、方法和构造方法,全面讲解反射在运行时检查与操作类内部结构的能力。通过实际代码示例展示反射在框架开发、序列化、配置驱动等场景中的关键作用,并探讨反射的性能开销、安全限制以及与JDK 21模块化的协同注意事项,帮助开发者掌握这一Java动态编程的强大工具。
关键词: Java反射, Class对象, 动态调用, 字段访问, 方法调用
在之前的文章中,我们一直在做一件事:写代码,然后编译运行。你定义一个类,写下它的字段和方法,然后创建对象,调用方法——这一切都在“编译时”就已经确定了。
但你想过没有:有没有可能在程序运行时,去“检查”一个类的内部结构?
比如,你有一个对象,但你不知道它是什么类型的。你能在运行时问它:“你是什么类?你有什么字段?有哪些方法?我能调用你的方法吗?”
这种能力,在 Java 中叫做 反射(Reflection)。
反射就像是一台“显微镜”或“X光机”,它让你在程序运行时,能够透视任意一个类的内部结构——无论这个类是 JDK 自带的,还是你自己写的,甚至是第三方库里的。
现代 Java 的很多基础设施都建立在反射之上:
- Spring 框架:用反射读取你写的
@Service、@Autowired注解,动态创建和管理对象。 - JUnit 测试框架:用反射找到所有标注了
@Test的方法,并动态调用它们。 - JSON 序列化库(如 Jackson、Gson):用反射读取对象的字段名和值,转换成 JSON 字符串。
- IDE 的自动补全和智能提示:也是通过反射(或更底层的机制)来分析代码结构。
今天,我们就来学习反射这个强大的工具。虽然你可能不会每天都在代码里直接用反射(它确实有点“重量级”),但理解它,能让你更深刻地理解 Java 的动态性,也让你在阅读框架源码时不再一头雾水。
1. 什么是反射?—— 运行时的“解剖刀”
反射(Reflection) 是 Java 语言的一种能力,它允许运行中的程序检查和操作类、接口、字段、方法的内部信息,而不需要依赖编译时的类型信息。
通俗地说:反射让你在程序运行时,能够“扒开”一个对象的外壳,看看它里面到底有什么,甚至可以动态地调用它的方法、修改它的字段——即使这些字段和方法是 private 的。
打个比方:你平时开车,是通过方向盘、油门、刹车来操控汽车——这是“正常调用”。而反射就像是,你把引擎盖打开,用工具直接去拧里面的螺丝、拉动里面的线路——这是“底层操作”。正常情况下你不应该这么做,但有时候(比如修车,即框架开发)你确实需要这种能力。
2. 获取 Class 对象 —— 反射的入口
要使用反射,你首先需要获得一个类的 Class 对象。Class 对象是反射的“总入口”,它包含了类的所有元数据——类名、包名、父类、字段、方法、构造方法、注解等等。
Java 提供了三种方式获取 Class 对象:
① 通过类名 .class
Class<String> clazz1 = String.class;
Class<Student> clazz2 = Student.class;
这种方式最简单,不需要创建对象。
② 通过对象的 getClass() 方法
Student s = new Student("张三", 20, "S001");
Class<?> clazz = s.getClass(); // 运行时实际类型
这种方式适合你不知道对象具体类型的情况。
③ 通过 Class.forName(String className)
try {
Class<?> clazz = Class.forName("java.util.ArrayList");
Class<?> clazz2 = Class.forName("com.example.demo.Student");
} catch (ClassNotFoundException e) {
// 类不存在时抛出
}
这种方式适合从配置文件中读取类名,动态加载。这是很多框架(如 JDBC 驱动加载)的经典用法。
注意:
Class.forName()会触发类的静态初始化(执行static块),而前两种方式不会。
3. 通过反射获取类的基本信息
有了 Class 对象,你可以获取类的各种元信息:
import java.lang.reflect.Modifier;
public class ReflectionDemo {
public static void main(String[] args) {
Class<?> clazz = Student.class;
// 1. 类名
System.out.println("类名:" + clazz.getName()); // 全限定名:com.example.Student
System.out.println("简单类名:" + clazz.getSimpleName()); // Student
// 2. 包名
System.out.println("包名:" + clazz.getPackage().getName());
// 3. 父类
Class<?> superClass = clazz.getSuperclass();
System.out.println("父类:" + (superClass != null ? superClass.getSimpleName() : "无"));
// 4. 实现的接口
Class<?>[] interfaces = clazz.getInterfaces();
System.out.println("实现的接口:");
for (Class<?> iface : interfaces) {
System.out.println(" " + iface.getSimpleName());
}
// 5. 修饰符
int modifiers = clazz.getModifiers();
System.out.println("是否是 public:" + Modifier.isPublic(modifiers));
System.out.println("是否是 abstract:" + Modifier.isAbstract(modifiers));
System.out.println("是否是 final:" + Modifier.isFinal(modifiers));
}
}
4. 通过反射访问字段
你可以通过反射获取类的所有字段(包括 private 的),并读取或修改它们的值。
4.1 获取字段
import java.lang.reflect.Field;
public class FieldReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Student.class;
// 1. 获取所有 public 字段(包括继承的)
Field[] publicFields = clazz.getFields();
// 2. 获取所有字段(包括 private、protected,但不包括继承的)
Field[] allFields = clazz.getDeclaredFields();
for (Field field : allFields) {
System.out.println("字段名:" + field.getName());
System.out.println(" 类型:" + field.getType().getSimpleName());
System.out.println(" 修饰符:" + Modifier.toString(field.getModifiers()));
}
// 3. 获取特定字段(按名称)
Field nameField = clazz.getDeclaredField("name"); // 假设 Student 有 name 字段
System.out.println("找到字段:name,类型:" + nameField.getType().getSimpleName());
}
}
4.2 读取和修改字段的值
public class FieldAccessDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Student s = new Student("张三", 20, "S001");
Class<?> clazz = s.getClass();
// 获取私有字段 name
Field nameField = clazz.getDeclaredField("name");
// 重要!访问私有字段需要设置可访问
nameField.setAccessible(true);
// 读取值
String name = (String) nameField.get(s);
System.out.println("原来姓名:" + name);
// 修改值
nameField.set(s, "李四");
System.out.println("修改后姓名:" + s.getName()); // 李四
// 获取 age 字段
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(s, 25); // 用 setInt 设置 int 类型
System.out.println("修改后年龄:" + s.getAge());
}
}
重要:调用 setAccessible(true) 可以绕过 private、protected 的访问限制。这是反射最强大的地方之一,也是最需要谨慎使用的功能——它破坏了封装性,可能带来安全隐患。
5. 通过反射访问方法
你可以获取类的所有方法,并动态调用它们(即使方法是 private 的)。
5.1 获取方法
import java.lang.reflect.Method;
public class MethodReflectionDemo {
public static void main(String[] args) {
Class<?> clazz = Student.class;
// 1. 获取所有 public 方法(包括继承的)
Method[] publicMethods = clazz.getMethods();
// 2. 获取所有方法(包括 private,不包括继承的)
Method[] allMethods = clazz.getDeclaredMethods();
for (Method method : allMethods) {
System.out.println("方法名:" + method.getName());
System.out.println(" 返回类型:" + method.getReturnType().getSimpleName());
System.out.println(" 参数类型:");
for (Class<?> param : method.getParameterTypes()) {
System.out.println(" " + param.getSimpleName());
}
System.out.println(" 修饰符:" + Modifier.toString(method.getModifiers()));
System.out.println("---");
}
// 3. 获取特定方法(需要指定方法名和参数类型)
Method sayHello = clazz.getDeclaredMethod("sayHello"); // 无参方法
Method setName = clazz.getDeclaredMethod("setName", String.class); // 有参方法
}
}
5.2 动态调用方法
public class MethodInvokeDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Student s = new Student("张三", 20, "S001");
Class<?> clazz = s.getClass();
// 1. 调用 public 方法
Method sayHello = clazz.getDeclaredMethod("sayHello");
sayHello.invoke(s); // 相当于 s.sayHello()
// 2. 调用带参数的方法
Method setName = clazz.getDeclaredMethod("setName", String.class);
setName.invoke(s, "王五"); // 相当于 s.setName("王五")
System.out.println("修改后:" + s.getName());
// 3. 调用 private 方法(需要 setAccessible)
Method privateMethod = clazz.getDeclaredMethod("privateHelper"); // 假设有这样一个私有方法
privateMethod.setAccessible(true);
Object result = privateMethod.invoke(s); // 如果有返回值
System.out.println("私有方法返回值:" + result);
// 4. 调用静态方法(传入 null 作为对象)
Method staticMethod = clazz.getDeclaredMethod("staticMethod", String.class);
staticMethod.invoke(null, "Hello"); // 相当于 ClassName.staticMethod("Hello")
}
}
Method.invoke(Object obj, Object... args) 的第一个参数是调用方法的对象(静态方法传 null),后面的参数是方法参数。
6. 通过反射操作构造方法
你可以用反射动态创建对象,而不使用 new 关键字。
import java.lang.reflect.Constructor;
public class ConstructorReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Student.class;
// 1. 获取所有构造方法
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> c : constructors) {
System.out.println("构造方法:" + c.getName());
System.out.println(" 参数类型:");
for (Class<?> param : c.getParameterTypes()) {
System.out.println(" " + param.getSimpleName());
}
}
// 2. 获取指定构造方法
Constructor<?> c1 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
// 如果构造方法是 private,需要 setAccessible
// 但通常是 public 的
// 3. 用构造方法创建对象
Student s = (Student) c1.newInstance("张三", 20, "S001");
System.out.println("通过反射创建的学生:" + s.getName());
// 4. 获取无参构造方法
Constructor<?> c2 = clazz.getDeclaredConstructor();
Student s2 = (Student) c2.newInstance();
System.out.println("通过无参构造创建:" + s2);
}
}
7. 反射与泛型 —— 类型擦除后的“残余信息”
我们之前学过,Java 的泛型有类型擦除——运行时,List<String> 和 List<Integer> 在 JVM 看来都是 List。但反射可以捕获到泛型的“残余信息”,比如通过 Field.getGenericType() 获取带泛型参数的类型。
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericReflectionDemo {
private List<String> names;
public static void main(String[] args) throws Exception {
Field field = GenericReflectionDemo.class.getDeclaredField("names");
// 普通类型
Class<?> fieldType = field.getType();
System.out.println("普通类型:" + fieldType.getSimpleName()); // List
// 泛型类型(保留泛型信息)
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) genericType;
System.out.println("原始类型:" + pt.getRawType().getTypeName()); // List
for (Type arg : pt.getActualTypeArguments()) {
System.out.println("泛型参数:" + arg.getTypeName()); // String
}
}
}
}
这种能力在 JSON 解析框架(如 Jackson)中非常关键——它需要知道 List<User> 里的元素是 User 类型,才能正确反序列化。
8. 反射的性能与安全
8.1 性能开销
反射比普通调用慢,原因包括:
Class对象的创建和元数据的加载setAccessible的检查- 方法的动态分派
- 编译器的优化受限
在性能敏感的代码中(比如每秒调用数百万次),应避免使用反射。但在大多数业务场景(如框架初始化、配置加载)中,反射的开销可以忽略。
粗略经验:反射调用比直接调用慢 2~5 倍。但如果你只调用一次或几次,完全不用担心。
8.2 安全限制
反射可以绕过 private 修饰符,这破坏了封装性。在安全管理器(SecurityManager)启用的情况下,某些反射操作会被禁止(比如访问 private 字段)。在大多数普通 Java 应用中,安全管理器默认是关闭的。
9. 反射的典型应用场景
| 场景 | 说明 |
|---|---|
| 框架开发 | Spring、JUnit、Hibernate 等通过反射读取注解、动态创建对象、注入依赖。 |
| 序列化/反序列化 | JSON/XML 库通过反射读取对象的字段名和值。 |
| 配置驱动的代码 | 从配置文件读取类名,用 Class.forName() 动态加载。 |
| 调试工具 | IDE 的变量查看器、JVM 的 JMX 工具等通过反射读取运行时对象的内部状态。 |
| 注解处理器 | 读取注解并进行处理(比如生成代码)。 |
| ORM 框架 | 将数据库表映射到 Java 对象,通过反射读取字段并赋值。 |
10. 综合示例 —— 一个简单的“对象转 JSON”工具
我们写一个小工具,把任意对象的字段(包括私有)转成 JSON 格式的字符串。
import java.lang.reflect.Field;
import java.util.*;
public class SimpleJsonSerializer {
/**
* 将对象序列化为 JSON 字符串(简化版,只处理基本类型和 String)
*/
public static String toJson(Object obj) throws IllegalAccessException {
if (obj == null) {
return "null";
}
Class<?> clazz = obj.getClass();
// 如果是基本类型、String、包装类,直接返回字符串表示
if (clazz.isPrimitive() || obj instanceof String ||
obj instanceof Number || obj instanceof Boolean) {
return String.valueOf(obj);
}
// 如果是 List/Set/数组,处理成 JSON 数组
if (obj instanceof Collection<?> collection) {
List<String> items = new ArrayList<>();
for (Object item : collection) {
items.add(toJson(item));
}
return "[" + String.join(",", items) + "]";
}
// 如果是 Map,处理成 JSON 对象
if (obj instanceof Map<?, ?> map) {
List<String> entries = new ArrayList<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
String value = toJson(entry.getValue());
entries.add("\"" + key + "\":" + value);
}
return "{" + String.join(",", entries) + "}";
}
// 普通对象:处理所有字段
Field[] fields = clazz.getDeclaredFields();
Map<String, String> jsonMap = new LinkedHashMap<>();
for (Field field : fields) {
// 跳过静态字段
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
Object value = field.get(obj);
jsonMap.put(field.getName(), toJson(value));
}
// 构建 JSON 字符串
List<String> pairs = new ArrayList<>();
for (Map.Entry<String, String> entry : jsonMap.entrySet()) {
pairs.add("\"" + entry.getKey() + "\":" + entry.getValue());
}
return "{" + String.join(",", pairs) + "}";
}
public static void main(String[] args) throws Exception {
// 测试用的数据类
class Address {
String city = "成都";
String street = "天府大道";
}
class Person {
String name = "张三";
int age = 20;
boolean active = true;
Address address = new Address();
List<String> hobbies = List.of("编程", "阅读", "跑步");
}
Person person = new Person();
String json = toJson(person);
System.out.println("JSON 输出:");
System.out.println(json);
// 输出(格式化后):
// {
// "name":"张三",
// "age":20,
// "active":true,
// "address":{
// "city":"成都",
// "street":"天府大道"
// },
// "hobbies":["编程","阅读","跑步"]
// }
}
}
这个例子展示了反射在序列化场景中的实际应用——我们不需要为每个类单独写序列化方法,反射可以通用地处理任何对象。
11. 反射与 JDK 21 的协同
JDK 21 中,反射依然是最重要的底层机制之一。但需要注意的是,随着模块化(JPMS)的普及,反射在模块间的访问受到了一些限制:
- 如果一个包没有被
exports导出,外部模块的反射无法访问它。 - 如果一个包被
opens开放,则允许反射访问(即使没有导出)。
在 module-info.java 中声明:
module com.example.demo {
// 导出包(正常访问)
exports com.example.demo.api;
// 开放包给反射
opens com.example.demo.model;
}
这意味着,在模块化项目中,反射不是“无限制”的,框架开发者需要明确声明哪些包可以被反射访问。你作为应用开发者,通常不需要太担心这个问题——框架会处理好。
12. 今天的总结
今天我们学习了反射的核心内容:
- 反射是什么:运行时检查、操作类、字段、方法的能力。
- 获取
Class对象:类名.class、对象.getClass()、Class.forName()。 - 获取类信息:类名、包名、父类、接口、修饰符。
- 操作字段:
getDeclaredFields()、setAccessible(true)、get()、set()。 - 操作方法:
getDeclaredMethods()、invoke()。 - 操作构造方法:
getDeclaredConstructors()、newInstance()。 - 反射与泛型:
getGenericType()获取泛型参数信息。 - 性能与安全:反射有开销,可绕过
private,需谨慎使用。 - 应用场景:框架、序列化、配置加载、调试工具。
- JDK 21 注意点:模块化对反射有一定限制。
反射是 Java 强大动态性的体现,也是很多高级框架的基石。虽然你可能不会每天都直接用它,但了解它,会让你在遇到“运行时动态处理”的问题时,多一把有力的武器。
动手试试
-
写一个方法
printClassInfo(Class<?> clazz),打印类的所有信息:类名、父类、接口、所有字段(含类型和修饰符)、所有方法(含返回类型、参数和修饰符)。 -
用反射创建一个
ArrayList对象,添加几个元素,然后遍历并打印这些元素(注意ArrayList的泛型被擦除了,但你可以通过反射调用add方法)。 -
给定一个对象,用反射遍历它的所有字段,把所有
String类型的字段值改为"modified"(提示:需要检查field.getType() == String.class)。 -
(挑战)写一个
@Log注解(运行时保留),用反射读取标注了这个注解的方法,并在方法执行前后打印日志。你需要用反射实现一个简单的“AOP”效果。
反射的内容就讲到这里了。下一篇文章,我们将学习注解——那些给代码“贴标签”的元数据机制,它是很多框架(包括 Spring)的核心工具。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。

1096

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



