# 2026-08-13 反射 Field 字段反射与 Method 方法反射练习(课堂练习用) > **说明**:练习今日新学知识点——**反射 Field(属性/字段/变量)**(Demo01:Class 类获取 Field 对象的 4 个 API——getFields / getField / getDeclaredFields / getDeclaredField、Field 类的常用方法——get() 读取 / set() 赋值 / setAccessible(true) 暴力反射 / getType() 类型 / getName() 属性名 / getModifiers() 修饰符、Modifier.toString 解析修饰符、getDeclaredFields 遍历输出全部字段的修饰符/类型/属性名、getField + get/set 读取修改 public 属性、getDeclaredField + setAccessible(true) 暴力反射读取修改 private 属性、操作静态属性 get/set 对象参数传 null、反射赋值与普通访问是同一份数据、ORM 框架用 Field.set() 把数据库数据映射到 Java 类的底层原理)+ **反射 Method(方法)**(Demo02:Class 类获取 Method 对象的 4 个 API——getMethods / getMethod / getDeclaredMethods / getDeclaredMethod、重载方法必须传参数类型区分、Method 类的常用方法——invoke 调用 / setAccessible(true) 暴力反射 / getReturnType / getParameterTypes / getModifiers、invoke 四种调用规律——实例方法传对象 / 静态方法传 null / void 返回 null / private 先暴力反射、动态代理 / 测试框架 / 注解解释器的基础)。**练习一~四**:Field 字段反射——通用工具遍历打印全部字段;读取/修改 public 字段;暴力反射读取/修改 private 字段;操作静态字段 + 模拟 ORM 映射(综合实战)。**练习五~八**:Method 方法反射——通用工具遍历打印全部方法;反射调用实例方法(无参/带参/重载);暴力反射调用私有方法与静态方法;模拟测试框架反射三件套综合实战。 --- ## 练习一:通用工具 —— 遍历打印任意类的所有字段 **难度**:⭐⭐⭐ **知识点**:`getDeclaredFields()`(本类全部字段,含 private、不含父类)、`field.getModifiers()` + `Modifier.toString()` 解析修饰符、`field.getType()` + `getSimpleName()` 获取类型、`field.getName()` 获取属性名、`clazz.getSimpleName()` 获取简单类名 **场景描述**:课堂 Demo01 演示① 在 main 方法里写死了"打印 Student 的全部字段"。但反射最大的价值是**通用**——本练习把"打印一个类所有字段信息"封装成**通用工具方法 `printFields(Class clazz)`**,传入**任意类**的 Class 对象,就能打印它的全部字段(修饰符 + 类型 + 属性名)。再分别对 `Student` 和 `String`(JDK 核心类!)调用,体会"反射不写死具体类、谁传谁打"。 ```java // 源码:course/Student.java(本练习的实体类,7 个字段:4 private + 2 public + 1 public static) public class Student{ private Integer id; private String name; private Integer age; private String gender; public String email; public String phone; public static String school; // …… 4 个构造方法 + study() 方法 } ``` ### 案例需求 写一个通用方法,传入任意类的 Class 对象,遍历打印其**所有字段**的「修饰符 + 类型 + 属性名」,分别对 Student.class 和 String.class 调用验证。 ### 实现步骤 1. 定义通用方法 `printFields(Class clazz)`——参数是任意类的 Class 对象 2. `clazz.getDeclaredFields()` 获取本类声明的**全部字段数组**(含 private,不含父类) 3. 遍历每个 Field 对象:`getModifiers()` 拿修饰符 int 值 → `Modifier.toString()` 解析成可读字符串(如 private / public static) 4. `getType().getSimpleName()` 获取类型名 + `getName()` 获取属性名 5. 拼接打印 `修饰符 + 空格 + 类型 + 空格 + 属性名` 6. main 里分别用 `printFields(Student.class)` 和 `printFields(String.class)` 调用 ### 代码框架 ```java package course; import java.lang.reflect.Field; import java.lang.reflect.Modifier; public class FieldPrintUtilTest { public static void main(String[] args) { // 1. 打印 Student 的全部字段 // 2. 打印 String 的全部字段(JDK 核心类,看看它有哪些字段) } // 通用工具:打印任意类 clazz 的所有字段(修饰符 + 类型 + 属性名) public static void printFields(Class clazz) { // 打印类名(clazz.getSimpleName()) // getDeclaredFields() 获取全部字段数组 // 遍历每个字段: // getModifiers() + Modifier.toString() 解析修饰符 // getType().getSimpleName() 获取类型名 // getName() 获取属性名 // 打印 } } ``` **输出示例**: ``` Student 类的所有字段: private Integer id private String name private Integer age private String gender public String email public String phone public static String school String 类的所有字段: private final byte[] value private final byte coder private int hash private boolean hashIsZero private static final long serialVersionUID ... ``` **选做加分**: - 同时用 `getFields()` 再打印一遍,对比与 `getDeclaredFields()` 的差异——Student 的哪些字段消失(private 的),`getFields()` 是否列出父类 Object 的字段(Object 没有 public 字段,可观察 List/Date 等有父类字段的类对比) - 打印字段个数:`getDeclaredFields().length`(Student 应为 7) - 直接打印 `getModifiers()` 的**原始 int 值**(如 private=2、public=1、public static=9),再用 `Modifier.toString()` 对比——体会"修饰符底层是 int 位掩码" **思考题**: 1. `getDeclaredFields()` 和 `getFields()` 有什么区别?(提示:Declared 拿本类全部含 private 不含父类;非 Declared 只拿 public 含父类继承——口诀"含 Declared 的能拿私有") 2. `getModifiers()` 返回的是 int,为什么需要 `Modifier.toString()` 解析?它能解析出哪些字符串?(提示:private / public / public static...) 3. `String.class` 是 JDK 核心类,为什么反射也能看到它的字段?(提示:反射对任意类都适用,包括 JDK 核心类) --- ## 练习二:反射读取/修改 public 字段 —— getField + get/set **难度**:⭐⭐⭐ **知识点**:`clazz.getField("字段名")` 获取指定 public 字段、`field.get(对象)` 读取属性值、`field.set(对象, 值)` 修改属性值、反射赋值与普通访问是同一份数据 **场景描述**:课堂 Demo01 演示② 用 `getField("email")` + `get()/set()` 读取、修改了 Student 的 public 字段 email。本练习要求:用反射读取/修改 Student 的 **email、phone 两个 public 字段**,修改后**用普通方式 `对象.字段名` 直接访问验证**(证明反射赋值与普通访问操作的是同一份数据),并把"读取/修改字段"封装成**通用方法 `readField` / `setField`**。 ### 案例需求 创建 Student 对象后,用反射读取 public 字段 email、phone,再反射修改它们,并用普通访问方式验证修改生效。 ### 实现步骤 1. `new Student(1001, "张三", 25, "男", "zhangsan@163.com", "13636363366")` 创建对象(六参构造,email/phone 构造里已赋值) 2. `clazz.getField("email")` 获取 email 字段的 Field 对象(getField 只能拿 public 字段) 3. `emailField.get(s)` 读取值、`emailField.set(s, "lisi@qq.com")` 修改值 4. **用 `s.email` 普通访问验证**——反射 set 后直接访问也看到新值 5. 同样操作 phone 字段 6. 封装通用方法 `readField(Object obj, String fieldName)` 和 `setField(Object obj, String fieldName, Object value)`,内部就是 `getField` + `get/set` ### 代码框架 ```java package course; import java.lang.reflect.Field; public class FieldPublicTest { public static void main(String[] args) throws Exception { // 1. 创建 Student 对象(六参构造) // 2. 反射读取 email、phone 并打印 // 3. 反射修改 email、phone // 4. 用 s.email / s.phone 普通访问验证修改生效 } // 通用工具:读取对象 obj 的 public 字段 fieldName 的值 public static Object readField(Object obj, String fieldName) throws Exception { // getField(fieldName) → field.get(obj) return null; } // 通用工具:给对象 obj 的 public 字段 fieldName 赋值 value public static void setField(Object obj, String fieldName, Object value) throws Exception { // getField(fieldName) → field.set(obj, value) } } ``` **输出示例**: ``` 读取 email:zhangsan@163.com 读取 phone:13636363366 反射修改后:lisi@qq.com / 13900000000 用 s.email 普通访问验证:lisi@qq.com ← 反射 set 与普通访问是同一份数据 用 s.phone 普通访问验证:13900000000 ``` **选做加分**: - 用 `getFields()` 遍历打印 Student 的**所有 public 字段名**(email、phone、school——school 是 public 也会被列出) - 用 `getField("id")`(private 字段)尝试获取,运行观察抛 **NoSuchFieldException**——验证"getField 只能拿 public"的口诀 - 封装好的 `readField` / `setField` 对 private 字段 name 调用试试,观察抛 **IllegalAccessException**——为练习三做铺垫 **思考题**: 1. `getField("字段名")` 里的字段名写错(不存在),会抛什么异常?(提示:NoSuchFieldException,受检异常) 2. 反射 `field.set(s, "lisi@qq.com")` 修改后,为什么 `s.email` 直接访问也变成新值?(提示:反射操作的就是对象的真实字段内存,同一份数据) 3. `field.get(obj)` 返回的是什么类型?给 String 字段取值需要强转吗?(提示:返回 Object,使用时通常强转) --- ## 练习三:暴力反射读取/修改 private 字段 —— getDeclaredField + setAccessible(true) **难度**:⭐⭐⭐⭐ **知识点**:`clazz.getDeclaredField("字段名")` 获取字段(**含 private**)、`field.setAccessible(true)` 暴力反射(临时取消访问权限检查)、`field.get/set` 读写、通用字段读写工具(不区分权限) **场景描述**:课堂 Demo01 演示③ 对 private 字段 name 做了暴力反射——**getDeclaredField 拿到 private 字段 → setAccessible(true) 放开权限 → get/set 操作**(这就是反射"破坏封装性"的体现)。本练习要求:写一个**不区分字段权限**的通用工具 `setFieldValue(obj, fieldName, value)` / `getFieldValue(obj, fieldName)`——内部流程 `getDeclaredField → setAccessible(true) → set/get`,用这个工具给 Student 的 **4 个 private 字段(id/name/age/gender)赋值并读取**。 ### 案例需求 写通用工具,能对**任意权限**(public/private)的字段赋值和读取;用它对 Student 的 4 个 private 字段赋值并读取验证。 ### 实现步骤 1. 创建 Student 空对象:`Class.forName("course.Student").getConstructor().newInstance()`(复用上一课反射创建对象) 2. 封装 `setFieldValue(Object obj, String fieldName, Object value)`:`getDeclaredField(fieldName)` → `setAccessible(true)` → `field.set(obj, value)` 3. 封装 `getFieldValue(Object obj, String fieldName)`:同样套路 → `field.get(obj)` 4. 用工具给 id / name / age / gender 四个 private 字段赋值 5. 用 `getFieldValue` 读取打印验证,强转成 Student 调用 `study()` 观察 name 生效 ### 代码框架 ```java package course; import java.lang.reflect.Field; public class FieldPrivateTest { public static void main(String[] args) throws Exception { // 1. 反射创建 Student 空对象(Class.forName + getConstructor().newInstance()) // 2. 用 setFieldValue 给 id / name / age / gender 四个 private 字段赋值 // 3. 用 getFieldValue 读取四个字段打印验证 // 4. 强转成 Student 调用 study(),观察 name 是否生效 } // 通用工具:给对象 obj 的任意权限字段 fieldName 赋值 value(getDeclaredField + setAccessible + set) public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { // getDeclaredField(fieldName) // setAccessible(true) // set(obj, value) } // 通用工具:读取对象 obj 的任意权限字段 fieldName 的值(getDeclaredField + setAccessible + get) public static Object getFieldValue(Object obj, String fieldName) throws Exception { // getDeclaredField(fieldName) // setAccessible(true) // get(obj) return null; } } ``` **输出示例**: ``` id = 1001 name = 李四 age = 20 gender = 男 李四在学习..... ← 反射给 private name 赋值生效,study() 输出 ``` **选做加分**: - **对比实验**:把 `setFieldValue` 里的 `setAccessible(true)` 注释掉,运行观察抛 **IllegalAccessException**——体会"private 字段不放开权限无法操作" - 用 `getField("name")`(不带 Declared)尝试获取 private 字段,观察抛 **NoSuchFieldException**——验证"不含 Declared 只能拿 public" - 在工具里加一行打印:`field.getType().getSimpleName()` + `field.getName()`,观察每次赋值时字段的类型名和属性名 **思考题**: 1. 为什么叫"暴力反射"?`setAccessible(true)` 做了什么?(提示:临时取消 Java 访问权限检查,private 也能访问) 2. private 字段在普通代码中编译期就报错,为什么反射能访问?(提示:反射是运行时动态机制,运行时可以绕过编译期/访问控制限制) 3. 反射能修改 private 字段,有什么隐患?(提示:破坏封装性、效率低 3~100 倍——回顾上一课"日常业务尽量不用反射") --- ## 练习四:综合实战 —— 操作静态字段(传 null)+ 模拟 ORM:数据库行 → Java 对象映射 **难度**:⭐⭐⭐⭐⭐ **知识点**:静态属性反射操作(`field.get(null)` / `field.set(null, 值)`——静态成员属于整个类、不传对象)、Field 反射全家桶(`getDeclaredFields` + `setAccessible` + `set`)、ORM 框架"数据库数据 → Java 类字段映射"底层原理(本课类注释:**"框架查询数据库,把数据库的数据一个一个映射到 Java 类中,用的就是 Field.set()"**)、`instanceof` 判断 + 强转 **场景描述**:这是本课最重要的知识落地——**ORM 框架(MyBatis/Hibernate)的底层映射原理**。框架查询数据库后,把**一行数据库记录**(字段名 → 值)映射成 **Java 对象**(属性名 → 值),靠的就是反射 `Field.set()`。本练习做两件事: 1. **操作静态字段**:用反射给 Student 的 `public static String school` 赋值/读取——注意**对象参数传 null**(静态成员属于整个类),再用 `Student.school` 直接访问验证 2. **模拟 ORM 映射**:写一个**通用方法 `mapToObject(Map row, Class clazz)`**——把模拟"数据库一行记录"的 Map 映射成任意类的对象:反射创建空对象 → 遍历该类全部字段 → 字段名在 row 中则 `field.set(obj, 值)`(自动包含 private,因为有 setAccessible)→ 返回映射好的对象 ### 案例需求 (1)用反射读取/修改静态字段 school(对象参数传 null),验证全类共享;(2)写通用方法 mapToObject,把"数据库行"Map 映射成 Student 对象(含 private 字段),调用 study() 验证对象可用。 ### 实现步骤 1. **静态字段部分**: - `clazz.getDeclaredField("school")` + `setAccessible(true)` 获取静态字段 - `schoolField.get(null)` 读取(**对象参数传 null**)、`schoolField.set(null, "哈尔滨佛学院")` 修改 - `Student.school` 直接访问验证——反射修改静态属性全类生效 2. **ORM 映射部分**: - 创建 Map 模拟数据库一行记录:`row.put("id", 1001)`、`row.put("name", "张三")`、`row.put("age", 25)`、`row.put("gender", "男")`、`row.put("email", "zhangsan@163.com")`、`row.put("phone", "13800000000")` - 写通用方法 `mapToObject(row, Student.class)`: - `clazz.getConstructor().newInstance()` 反射创建空对象 - `getDeclaredFields()` 遍历全部字段 - 字段名在 row 里 → `setAccessible(true)` → `field.set(obj, row.get(fieldName))` ← **映射核心** - 用练习三的工具 `getFieldValue` 读取映射后的字段打印验证 - `(Student) obj` 强转 + `study()` 调用验证 ### 代码框架 ```java package course; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class OrmFieldMapTest { public static void main(String[] args) throws Exception { // ===== 第 1 部分:操作静态字段 school(对象参数传 null)===== // 1. 获取 school 字段 + 放开访问权限 // 2. schoolField.get(null) 读取、schoolField.set(null, "哈尔滨佛学院") 修改 // 3. Student.school 直接访问验证 // ===== 第 2 部分:模拟 ORM —— 数据库行 → Java 对象 ===== // 4. 创建 Map 模拟数据库一行记录(key=字段名,value=值) Map row = new HashMap<>(); // row.put("id", 1001); row.put("name", "张三"); ... // 5. 调用通用方法,把"数据库行"映射成 Student 对象 Object obj = mapToObject(row, Student.class); // 6. 打印映射结果 + 强转调用 study() 验证 } // 通用映射方法:把 Map(数据库一行记录)映射成 clazz 的对象 —— ORM 底层原理 public static Object mapToObject(Map row, Class clazz) throws Exception { // 1. 反射创建空对象(getConstructor().newInstance()) // 2. getDeclaredFields() 遍历该类全部字段 // 3. 字段名在 row 里 → setAccessible(true) → field.set(obj, row.get(fieldName)) // 4. 返回映射好的对象 return null; } } ``` **输出示例**: ``` 静态属性 school:哈尔滨佛学院 ← schoolField.get(null) 读取 修改后 Student.school:福建桥梁职业技术学院 ← 反射 set(null, ...) 后直接访问验证 映射结果: id = 1001 name = 张三 age = 25 gender = 男 email = zhangsan@163.com phone = 13800000000 张三在学习..... ← 映射后的对象调用 study() 正常 ``` **选做加分**: - **换实体类不改代码**:再定义一个 `Teacher` 类(含 private name、public teach() 方法),用**同一个 `mapToObject`** 传入 `Teacher.class` 和一个对应的 row——代码一行不改完成映射——体会"反射不写死类"的通用威力 - **字段缺失跳过**:把 row 里故意少放一个字段(如不放 age),观察 mapToObject 自动跳过(字段不在 row 里就不赋值)——对应数据库某列为 NULL 的场景 - **静态字段进阶**:思考如果 school 是全校统一的静态字段,如何用 `field.set(null, ...)` 一次性统一赋值(所有对象共享一份) - 打印映射过程中每个字段的**处理日志**:`"正在映射字段:name = 张三"`——直观看到 ORM 逐字段映射的过程 **思考题**: 1. 反射操作**静态字段**时,`get/set` 的对象参数为什么传 **null**?(提示:静态成员属于整个类、不属于任何对象——本课核心知识点) 2. 为什么 ORM 框架要用反射 `Field.set()` 来映射数据库数据,而不是直接调用 setter?(提示:框架在编译期不知道实体类有哪些属性、有哪些 setter,靠反射运行时动态拿到——配置/注解驱动的解耦) 3. 模拟的 `mapToObject` 里没有 setter、没有写死 Student 的任何一个属性名,为什么能把 private 字段都赋上值?(提示:getDeclaredFields 拿全部字段 + setAccessible(true) 放开权限) 4. 如果数据库字段叫 `user_name`、Java 属性叫 `userName`(下划线 vs 驼峰),mapToObject 直接按字段名匹配会失败——你会怎么解决?(提示:字段名转换——把下划线转驼峰再匹配,这就是 MyBatis 的 mapUnderscoreToCamelCase 配置的底层逻辑) --- ## 练习五:通用工具 —— 遍历打印任意类的所有方法 **难度**:⭐⭐⭐ **知识点**:`getMethods()`(所有 public 方法,含父类/接口继承如 toString/hashCode)、`getDeclaredMethods()`(本类全部含 private,不含父类/接口)、`method.getParameterTypes()` 参数类型数组 + Stream 流映射、`method.getReturnType()` 返回值类型、`method.getName()` 方法名 **场景描述**:课堂 Demo02 演示① 遍历打印了 Student 的所有方法。本练习把"打印一个类的所有方法"封装成**通用工具方法**——分别打印 **public 方法(含父类 Object 继承的 toString 等)** 和 **本类全部方法(含 private 的 read/write)**,输出格式:`方法名(参数类型列表) 返回值=xxx`。对 Student 调用,观察 `getMethods()` 里哪些是 Object 继承来的(toString/hashCode/equals/getClass),`getDeclaredMethods()` 里多出哪些私有方法(read/write)。 ### 案例需求 写两个通用方法:`printPublicMethods(Class clazz)` 打印所有 public 方法(含继承)、`printDeclaredMethods(Class clazz)` 打印本类全部方法(含 private),对 Student.class 调用并对比差异。 ### 实现步骤 1. 定义 `printPublicMethods(Class clazz)`:`clazz.getMethods()` 遍历所有 public 方法 2. 每个方法:`getName()` 方法名 + `getParameterTypes()` 参数类型数组(`Arrays.stream(...)` 用 Stream 流映射成 `List` 简单名)+ `getReturnType().getSimpleName()` 返回值类型 3. 定义 `printDeclaredMethods(Class clazz)`:`clazz.getDeclaredMethods()` 遍历本类全部方法(含 private),同样的打印格式 4. main 里用 `Student.class` 分别调用,对比两组输出差异 5. 数一数 `getMethods()` 里有多少方法是 Object 继承来的(toString/hashCode/equals/getClass/wait/notify/notifyAll) ### 代码框架 ```java package course; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class MethodPrintUtilTest { public static void main(String[] args) { // 1. 打印 Student 的所有 public 方法(含父类 Object 继承的 toString 等) // 2. 打印 Student 的本类全部方法(含 private 的 read/write) } // 通用工具:打印任意类 clazz 的所有 public 方法(方法名(参数类型列表) 返回值=xxx) public static void printPublicMethods(Class clazz) { // getMethods() 遍历 // getParameterTypes() → Arrays.stream + map(getSimpleName) + collect(List) // getName() / getReturnType().getSimpleName() 拼接打印 } // 通用工具:打印任意类 clazz 的本类全部方法(含 private) public static void printDeclaredMethods(Class clazz) { // getDeclaredMethods() 遍历,同样的打印格式 } } ``` **输出示例**: ``` ========Student 的 public 方法(含父类/接口继承)======== getId([]) 返回值=Integer setId([Integer]) 返回值=void getName([]) 返回值=String setName([String]) 返回值=void study([]) 返回值=void toString([]) 返回值=String ← Object 继承来的 hashCode([]) 返回值=int ← Object 继承来的 equals([Object]) 返回值=boolean ← Object 继承来的 getClass([]) 返回值=Class ← Object 继承来的 ... ========Student 的本类全部方法(含 private)======== setName([String]) 返回值=void read([]) 返回值=void ← private 方法(getMethods 里看不到) write([]) 返回值=void ← private static 方法 study([]) 返回值=void ... ``` **选做加分**: - 打印方法个数:`getMethods().length` vs `getDeclaredMethods().length` 对比(public 含继承的更多) - 用 `method.getModifiers()` + `Modifier.toString()` 打印每个方法的**修饰符**(public/private/static) - 统计 public 方法里哪些是 Object 继承来的——手动挑出 toString/hashCode/equals/getClass/wait/notify/notifyAll,数一数有几个 **思考题**: 1. `getMethods()` 和 `getDeclaredMethods()` 有什么区别?(提示:含 Declared 的本类全部含 private、不含父类/接口;不含 Declared 的 public 含父类/接口继承——口诀与 Field/Constructor 一致) 2. 为什么 `getMethods()` 能看到 `toString` 而 `getDeclaredMethods()` 看不到?(提示:toString 是父类 Object 的方法,Declared 只拿本类声明) 3. `getParameterTypes()` 返回的是什么类型?为什么要用 Stream 流转成字符串?(提示:返回 `Class[]`,直接打印是 `[class java.lang.String]`,映射成简单名更易读) --- ## 练习六:反射调用实例方法 —— getMethod + invoke(无参/带参/重载) **难度**:⭐⭐⭐⭐ **知识点**:`getMethod("方法名")` 获取无参方法、`getMethod("方法名", 参数类型...)` 获取带参方法(**重载必须指明参数类型**,不写/写错抛 NoSuchMethodException)、`method.invoke(对象)` 调用无参方法、`invoke(对象, 参数...)` 传参调用、**void 方法 invoke 返回 null**、invoke 返回值强转 **场景描述**:课堂 Demo02 演示② 用 `getMethod("getName")` + `invoke(s)` 调用了无参方法,用 `getMethod("setName", String.class)` + `invoke(s, "李四")` 调用了带参方法。本练习把"调用方法"封装成**通用工具 `invokeMethod(Object obj, String methodName, Object... args)`**——不写死方法名,通过参数反射调用任意 public 实例方法;再用它调用 getName(无参有返回值)和 setName(带参 void)验证,并体会 **void 方法 invoke 返回 null**。 ### 案例需求 封装通用方法反射调用工具,用它调用 Student 的 getName()(无参有返回值)和 setName(String)(带参 void),并验证 void 方法的 invoke 返回值。 ### 实现步骤 1. 创建 Student 对象(六参构造,name="张三") 2. `clazz.getMethod("getName")` 获取无参方法 → `invoke(s)` 调用,**返回值强转 String** 打印 3. `clazz.getMethod("setName", String.class)` 获取带参方法(**重载必须传参数类型**)→ `invoke(s, "李四")` 调用(**void 方法不接收返回值**) 4. 用 `s.getName()` 普通访问验证反射 setter 调用生效 5. 封装通用方法 `invokeMethod(Object obj, String methodName, Object... args)`:根据 args 动态计算参数类型数组 → `getMethod` → `invoke(obj, args)` 6. 打印 `invokeMethod(s, "setName", "王五")` 的返回值——观察 **void 方法返回 null** ### 代码框架 ```java package course; import java.lang.reflect.Method; public class MethodInvokeTest { public static void main(String[] args) throws Exception { // 1. 创建 Student 对象(六参构造 name="张三") // 2. 反射调用 getName() 无参方法,接收返回值强转打印 // 3. 反射调用 setName(String) 带参方法(void 不接收返回值) // 4. 用 s.getName() 普通访问验证反射 setter 生效 // 5. 用封装好的 invokeMethod 工具再调用 setName // 6. 打印 invokeMethod(s, "setName", "王五") 的返回值 → null(void 方法) } // 通用工具:反射调用对象 obj 的 public 方法 methodName,参数 args,返回方法执行结果 public static Object invokeMethod(Object obj, String methodName, Object... args) throws Exception { // 1. 根据 args 动态计算参数类型数组(每个参数.getClass()) // 2. getMethod(methodName, 参数类型...) 获取方法 // 3. method.invoke(obj, args) 调用并返回结果 return null; } } ``` **输出示例**: ``` 反射调用 getName():张三 反射调用 setName("李四") 后:李四 通用工具调用 setName("王五") 后:王五 void 方法 invoke 返回值:null ``` **选做加分**: - **重载实验**:给 Student 重载一个 `setName(String, Integer)` 方法,用 invokeMethod 分别调用两个重载版本——体会"重载方法必须传参数类型区分" - 用 `getMethod("setName")`(不写参数类型)尝试获取,运行观察抛 **NoSuchMethodException**——验证"重载方法不指明参数类型找不到" - **参数类型不匹配实验**:调用 `invokeMethod(s, "setAge", "abc")`(age 是 Integer,传 String),观察 invoke 时抛 **IllegalArgumentException**(参数类型不匹配) **思考题**: 1. `invoke` 的第一个参数是什么?为什么调用实例方法必须传对象?(提示:实例方法属于某个对象,必须指明"哪个对象的方法") 2. **void 方法**调用 `invoke` 返回什么?有返回值的方法怎么拿结果?(提示:void 返回 null;有返回值返回 Object,可强转成目标类型) 3. **重载方法**为什么 `getMethod` 必须传参数类型?(提示:方法名相同、参数列表不同,`String.class` 用来区分是哪个重载版本) --- ## 练习七:暴力反射调用私有方法与静态方法 —— getDeclaredMethod + setAccessible(true) + invoke(null) **难度**:⭐⭐⭐⭐ **知识点**:`getDeclaredMethod("方法名")` 获取私有方法、`setAccessible(true)` 暴力反射、`invoke(对象)` 调用私有实例方法、**`invoke(null)` 调用静态方法**(静态方法 invoke 传 null,与 Field 操作静态属性传 null 同一规律)、不区分权限的通用方法调用工具 **场景描述**:课堂 Demo02 演示③④ 调用了 Student 的私有方法 `read()`(实例方法)和 `write()`(静态方法)。本练习写**不区分权限**的通用工具 `invokeAnyMethod(Object obj, String methodName, Object... args)`——内部流程 `getDeclaredMethod → setAccessible(true) → invoke`,用它调用 private read()(**实例方法 invoke 传对象 s**)和 private static write()(**静态方法 invoke 传 null**)——完整体会 **invoke 四种调用规律**中的"实例方法传对象、静态方法传 null、私有方法先暴力反射"。 ### 案例需求 写通用工具能调用任意权限方法,用它调用 Student 的 private read()(实例方法)和 private static write()(静态方法),并做暴力反射对比实验。 ### 实现步骤 1. 创建 Student 对象,反射调用 setName("张三")(复用练习六的工具) 2. `clazz.getDeclaredMethod("read")` 获取私有实例方法 → `setAccessible(true)` → `invoke(s)` 调用(**传对象 s**) 3. `clazz.getDeclaredMethod("write")` 获取私有静态方法 → `setAccessible(true)` → `invoke(null)` 调用(**传 null**) 4. 封装通用工具 `invokeAnyMethod(Object obj, String methodName, Object... args)`:`getDeclaredMethod` + `setAccessible(true)` + `invoke(obj, args)` 5. 用工具调用 read(obj 传 s)、write(obj 传 null)验证 6. **对比实验**:把工具里的 `setAccessible(true)` 注释掉,运行观察异常 ### 代码框架 ```java package course; import java.lang.reflect.Method; public class MethodPrivateTest { public static void main(String[] args) throws Exception { // 1. 创建 Student 对象,反射调用 setName("张三")(复用练习六工具) // 2. 暴力反射调用私有实例方法 read()(invoke 传对象 s) // 3. 暴力反射调用私有静态方法 write()(invoke 传 null) // 4. 用封装好的 invokeAnyMethod 工具调用 read(obj 传 s)/ write(obj 传 null) } // 通用工具:反射调用任意权限方法(getDeclaredMethod + setAccessible + invoke) public static Object invokeAnyMethod(Object obj, String methodName, Object... args) throws Exception { // 1. 计算参数类型数组 // 2. getDeclaredMethod(methodName, 参数类型...) 获取方法(含 private) // 3. setAccessible(true) 暴力反射放开权限 // 4. invoke(obj, args) 调用并返回结果 return null; } } ``` **输出示例**: ``` 张三读....... ← 暴力反射调用 private read()(实例方法,invoke 传对象 s,name 已是"张三") 静态方法...写............... ← 暴力反射调用 private static write()(静态方法,invoke 传 null) 张三读....... ← 通用工具调用 read()(obj 传 s) 静态方法...写............... ← 通用工具调用 write()(obj 传 null) ``` **选做加分**: - **对比实验(必做)**:把 `invokeAnyMethod` 里的 `setAccessible(true)` 注释掉,运行观察抛 **IllegalAccessException**——体会"私有方法不放开权限无法 invoke" - 用 `getMethod("read")`(不带 Declared)尝试获取私有方法,运行观察抛 **NoSuchMethodException**——验证"getMethod 只能拿 public"的口诀 - **静态方法传对象实验**:write() 是静态方法,invoke 第一参数传对象 s 试试(`invoke(s)`),观察是否也能执行——体会静态方法不依赖对象,传什么都会被忽略 - 用 `method.getModifiers()` + `Modifier.toString()` 打印 read/write 的修饰符,观察 `private` / `private static` **思考题**: 1. 为什么私有方法必须 `setAccessible(true)` 才能 invoke?(提示:Java 访问权限检查,不放开抛 IllegalAccessException——暴力反射) 2. 静态方法 invoke 为什么传 **null**?与 Field 操作静态属性传 null 是什么关系?(提示:静态成员属于整个类、不属于任何对象——本课核心规律统一) 3. `getDeclaredMethod` 与 `getMethod` 的区别?调用私有方法为什么必须用前者?(提示:口诀"含 Declared 的能拿私有") --- ## 练习八:综合实战 —— 模拟测试框架(JUnit 简化版):反射三件套扫描并调用私有测试方法 **难度**:⭐⭐⭐⭐⭐ **知识点**:反射三件套全家桶(Constructor 创建对象 + Field 给字段赋值 + Method 调用方法)、`getDeclaredMethods()` 遍历扫描方法、私有方法暴力反射、**`InvocationTargetException`**(invoke 包装被调方法内部异常,`getCause()` 取真实异常)、`method.invoke` 返回值、`startsWith("test")` 过滤测试方法 **场景描述**:这是"Method 是测试框架、注解解释器的基础"的完整落地——**JUnit 测试框架的底层原理**:扫描一个类的所有方法,找出测试方法,反射创建对象后逐个调用。本练习给 Student 添加几个 **private 测试方法**(testAdd / testHello / testFail),写一个"**迷你测试框架** `MiniJUnitTest`": 1. **反射创建被测对象**(Constructor) 2. **getDeclaredMethods() 扫描**所有方法,找出名字以 `test` 开头的方法 3. 每个测试方法 **setAccessible(true) + invoke(对象)** 执行(Method) 4. 方法内部抛异常 → invoke 抛 **InvocationTargetException**,`getCause()` 打印真实异常并统计"测试失败" ——完整走一遍"反射三件套(创建对象/调方法)+ 暴力反射 + 异常处理"的**框架级流程**。 ### 案例需求 写一个迷你测试框架:反射创建 Student 对象,扫描所有以 test 开头的私有方法并暴力反射调用,统计测试通过/失败个数(方法抛异常算失败,用 InvocationTargetException.getCause() 打印原因)。 ### 实现步骤 1. 给 Student 增加 3 个 private 测试方法:`testAdd()`(正常通过)、`testHello()`(正常通过)、`testFail()`(故意抛异常) 2. `Student.class.getConstructor().newInstance()` 反射创建被测对象 3. `clazz.getDeclaredMethods()` 遍历 Student 全部方法 4. 用 `method.getName().startsWith("test")` 找出测试方法(过滤掉 read/write/study/getXxx 等) 5. 对每个测试方法:`setAccessible(true)` + `invoke(obj)` 执行 6. **try-catch 捕获 `InvocationTargetException`** → `getCause()` 打印真实异常信息 7. 统计"测试通过 / 测试失败"个数 ### 代码框架 ```java package course; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MiniJUnitTest { public static void main(String[] args) throws Exception { // 1. 反射创建被测 Student 对象(getConstructor().newInstance()) // 2. getDeclaredMethods() 遍历 Student 全部方法 // 3. 方法名以 "test" 开头 → 认定为测试方法 // 4. 对每个测试方法:setAccessible(true) + invoke(obj) 执行 // 5. 捕获 InvocationTargetException → getCause() 打印真实异常 // 6. 统计通过/失败个数 } } ``` **Student 补充测试方法**(在被测类 course/Student.java 中补充): ```java // 来源:course/Student.java(模拟被测类的测试方法) private void testAdd() { System.out.println("测试:加法计算"); int a = 1 + 1; if (a != 2) throw new RuntimeException("加法计算错误"); } private void testHello() { System.out.println("测试:打招呼 " + this.name); } private void testFail() { throw new RuntimeException("故意失败的方法"); } ``` **输出示例**: ``` 找到测试方法:testAdd 测试通过:testAdd 找到测试方法:testHello 测试通过:testHello 找到测试方法:testFail 测试失败:testFail,原因:故意失败的方法 测试通过 2 个,失败 1 个 ``` **选做加分**: - **getMethods() 扫描对比**:把测试方法改成 public、改用 `getMethods()` 扫描——观察 toString/hashCode 等 Object 继承方法也会被扫到,思考怎么过滤掉(提示:`!method.getName().equals("toString")` 或判断 `getDeclaringClass()`) - **给测试方法传参**:写一个带参数的测试方法 `testAdd(int a, int b)`,用 `invoke(obj, 3, 5)` 传参调用——体会"参数如何通过 invoke 传入" - **返回值处理**:让 test 方法返回 `boolean`(true 通过 false 失败),`invoke` 返回 Object 强转 boolean 判断——体会"invoke 返回方法执行结果" - **注解模拟(进阶)**:定义自定义注解 `@MyTest`,扫描时用 `method.isAnnotationPresent(MyTest.class)` 只调用带注解的方法——这就是 JUnit 的 `@Test` 注解底层原理 **思考题**: 1. 测试框架为什么要用**反射扫描方法**,而不是直接 `obj.testAdd()` 调用?(提示:框架在编译期不知道被测类有哪些测试方法——运行期动态扫描,这就是"为什么需要反射") 2. 方法内部抛异常时,为什么 `invoke` 抛的是 **InvocationTargetException** 而不是原异常?(提示:invoke 把被调方法内部异常**包装**了一层,需要 `getCause()` 取真正的异常对象) 3. 这个"迷你测试框架"和真实的 JUnit 有什么相似之处?(提示:JUnit 也是扫描类的方法 + 反射创建对象 + 反射调用;JUnit 用 @Test 注解标记测试方法、本练习用 test 前缀过滤) 4. 如果把测试方法改名成非 test 开头(如 study),扫描时会被执行吗?为什么?(提示:`startsWith("test")` 过滤——只有 test 开头的方法才会被认定为测试方法) --- # 涵盖知识点总览 | 知识点 | 说明 | |--------|------| | `getDeclaredFields()` | 返回本类(不含父类)声明的**所有字段**数组,**包括 private**——遍历可打印全部字段 | | `getFields()` | 返回所有 **public** 字段数组,**包括从父类继承的**——与 getDeclaredFields 的核心区别 | | `getField("字段名")` | 返回指定的单个 **public** 字段,字段名不存在抛 NoSuchFieldException | | `getDeclaredField("字段名")` | 返回指定的单个字段(**包括 private**)——操作私有字段必须用它 | | **Declared 口诀** | 含 Declared 的能拿私有(本类全部、不含父类);不含 Declared 的只能拿 public(含父类继承) | | `field.get(对象)` | 读取对象 obj 的该属性值,返回 Object(使用时常强转) | | `field.set(对象, 值)` | 设置对象 obj 的该属性值(参数:目标对象 + 要赋的值) | | `field.setAccessible(true)` | **暴力反射**:临时取消访问权限检查——操作 private 字段必须调用,否则抛 IllegalAccessException | | `field.getType()` + `getSimpleName()` | 获取属性类型(如 Integer/String) | | `field.getName()` | 获取属性名 | | `field.getModifiers()` + `Modifier.toString()` | 获取修饰符 int 值并解析成可读字符串(private / public / public static...) | | **操作静态字段传 null** | 静态成员属于整个类、不属于任何对象——`field.get(null)` / `field.set(null, 值)`;修改后 `类名.属性名` 直接访问全类生效 | | **反射赋值与普通访问同一份数据** | `field.set(obj, ...)` 修改后,`obj.属性名` 直接访问也看到新值——反射操作的就是对象真实字段内存 | | **ORM 映射底层原理** | 框架查询数据库后,用 `Field.set()` 把数据库数据一个一个映射到 Java 类中(MyBatis/Hibernate 结果映射) | | **通用映射 mapToObject** | 反射创建空对象 + `getDeclaredFields` 遍历 + 字段名匹配 row 的 key + `setAccessible(true)` + `field.set`——模拟数据库行 → Java 对象 | | **反射的通用性** | 工具方法传入任意 Class/对象都能工作(打印任意类字段、读写任意对象属性)——不写死具体类,配置驱动 + 解耦 | | **反射的注意事项** | 日常业务尽量不用:破坏封装性、效率低 3~100 倍;学习反射为理解框架底层源码(ORM/IOC) | | `getMethods()` | 返回**所有 public 方法**数组,**包括父类/接口继承的**(如 toString/hashCode/getClass) | | `getMethod(name, parameterTypes)` | 返回**指定的单个 public 方法**;**重载方法必须传参数类型区分**(如 `getMethod("setName", String.class)`),不写/写错抛 NoSuchMethodException | | `getDeclaredMethods()` | 返回**本类所有方法**数组,**包括 private,但不包括父类/接口的方法** | | `getDeclaredMethod(name, parameterTypes)` | 返回**指定的单个方法**,**包括 private**——操作私有方法必须用它 | | `method.invoke(obj, 参数...)` | **调用方法**,返回方法执行结果(Object 可强转);实例方法第一参数传对象、**静态方法传 null**、**void 方法返回 null**、**private 先 setAccessible(true)** | | `method.setAccessible(true)` | **暴力反射**:临时取消访问权限检查——调用 private 方法必须调用,否则抛 IllegalAccessException | | `method.getReturnType()` / `getParameterTypes()` / `getModifiers()` | 获取返回值类型(`getSimpleName()` 得简单名)/ 参数类型数组(Stream 映射简单名)/ 修饰符(Modifier.toString 解析) | | **invoke 四种调用规律** | ①实例方法 `invoke(对象,参数...)` 传对象 ②静态方法 `invoke(null,参数...)` 传 null ③void 方法 invoke 返回 null ④private 方法先 setAccessible(true) | | **遍历打印方法** | `getMethods()`/`getDeclaredMethods()` 遍历 + `getParameterTypes()` + `getReturnType().getSimpleName()` 输出方法名/参数/返回值——对比含继承 vs 本类全部 | | **反射调用 setter/getter** | `getMethod("setName", String.class)` + `invoke(obj, "李四")` 反射调用 setter 赋值(void 不接收返回值)、`getMethod("getName")` + `invoke(obj)` 调用 getter 取返回值——框架 BeanUtils 的底层 | | **InvocationTargetException** | invoke 调用方法时被调方法内部抛异常,invoke 会把它**包装成 InvocationTargetException** 抛出——需要 `getCause()` 获取真正的异常 | | **模拟测试框架(JUnit 原理)** | `getDeclaredMethods()` 扫描类的方法 → `startsWith("test")` 过滤测试方法 → `setAccessible(true)` + `invoke(obj)` 执行 → 捕获 InvocationTargetException 统计通过/失败——JUnit 扫描 @Test 注解方法的底层套路 | | **反射三件套组合** | Constructor(newInstance 创建对象)+ Field(get/set 读写字段)+ Method(invoke 调用方法)——三者组合 = MyBatis 结果映射 / Spring Bean 初始化 / 测试框架执行测试用例的完整底层链路 | | **反射 API 的受检异常** | NoSuchFieldException(字段名不存在)/ NoSuchMethodException(方法名或参数类型不匹配)/ IllegalAccessException(private 未放开权限)/ InvocationTargetException(被调方法内部抛异常)/ ClassNotFoundException(类名写错)——必须处理(main 用 throws) | | **反射 API 的受检异常** | NoSuchFieldException(字段名不存在)/ IllegalAccessException(private 未放开权限)——必须处理(main 用 throws) |