Field与Method详解
大翔在代码评审会上指着朱璐的代码:"朱璐,你这个
BeanUtils.copyProperties底层就是通过反射的 Field 和 Method 实现的,你知道它的性能开销吗?"朱璐一愣:"不就是复制属性吗,还能有什么坑?"
白歌接过话头:"我来解释。
Field.setAccessible(true)虽然能突破私有访问限制,但每次调用都要做安全检查,不缓存的话性能会差很多。"小崔在旁边听得云里雾里:"setAccessible 是什么意思?为什么要突破私有?"
Frank 总监走过来说:"小崔问得好。这正是反射的核心问题——如何在运行时操作类的内部结构。大翔,你来给小崔讲透。"
一、定义表
| 类/概念 | 描述 | 关键方法 |
|---|---|---|
Class | 代表一个类的运行时信息 | getFields(), getDeclaredFields(), getMethods(), getDeclaredMethods() |
Field | 代表类的成员变量(字段) | get(Object), set(Object, value), setAccessible(true), getName(), getType() |
Method | 代表类的方法 | invoke(Object, args...), setAccessible(true), getName(), getReturnType(), getParameterTypes() |
AccessibleObject | Field 和 Method 的父类 | setAccessible(boolean), isAccessible() |
Modifier | 解析修饰符(public/private/static等) | isPublic(int), isPrivate(int), isStatic(int), isFinal(int) |
二、getFields() vs getDeclaredFields() 对比
| 方法 | 返回范围 | 是否包含私有 | 是否包含继承 |
|---|---|---|---|
getFields() | 当前类 + 父类 + 接口中所有 public 字段 | 否 | 是 |
getDeclaredFields() | 当前类中所有访问级别的字段 | 是 | 否 |
getMethods() | 当前类 + 父类 + 接口中所有 public 方法(含继承的 Object 方法) | 否 | 是 |
getDeclaredMethods() | 当前类中所有访问级别的方法(不含继承) | 是 | 否 |
三、Mermaid 类图:反射核心类层次
四、Mermaid 时序图:Method.invoke() 执行流程
五、Field 核心操作详解
5.1 获取 Field 对象
Class<Employee> clazz = Employee.class;
// 获取所有 public 字段(含继承的)
Field[] publicFields = clazz.getFields();
// 获取当前类所有字段(含 private,不含继承)
Field[] allFields = clazz.getDeclaredFields();
// 获取指定名称的 public 字段
Field nameField = clazz.getField("name"); // 仅 public
// 获取指定名称的字段(任何访问级别)
Field salaryField = clazz.getDeclaredField("salary"); // 含 private
5.2 读取和设置字段值
Field salaryField = clazz.getDeclaredField("salary");
salaryField.setAccessible(true); // 突破 private 限制
// 读取
double salary = (double) salaryField.get(employeeInstance);
// 设置
salaryField.set(employeeInstance, 18888.88);
5.3 setAccessible 与安全性
setAccessible(true) 会绕过 Java 访问控制检查。如果在没有配置 --add-opens 的情况下(JDK 9+)访问 JDK 内部类,会抛出 InaccessibleObjectException。在 JDK 8 中,只要没有 SecurityManager,通常都可以成功 setAccessible。
六、Method 核心操作详解
6.1 获取 Method 对象
// 获取无参方法
Method getName = clazz.getDeclaredMethod("getName");
// 获取有参方法
Method setName = clazz.getDeclaredMethod("setName", String.class);
// 获取所有 public 方法(含继承的 Object 方法)
Method[] publicMethods = clazz.getMethods();
6.2 invoke 调用方法
Method setName = clazz.getDeclaredMethod("setName", String.class);
setName.invoke(employeeInstance, "大翔");
Method getName = clazz.getDeclaredMethod("getName");
String name = (String) getName.invoke(employeeInstance);
6.3 invoke 方法签名详解
public Object invoke(Object obj, Object... args)
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException
- 第一个参数
obj:目标对象实例。如果是静态方法,传null。 - 第二个参数
args:可变参数,方法实参列表。 - 返回值:如果方法返回 void,invoke 返回
null;基本类型会自动装箱。 - InvocationTargetException:如果被调用的方法内部抛出异常,会被包装为 InvocationTargetException。可通过
getCause()获取原始异常。
七、完整代码示例
示例1:飞翔科技通用对象属性复制工具
import java.lang.reflect.Field;
import java.util.*;
/**
* 飞翔科技通用对象属性复制工具 —— 基于 Field 反射
* 将源对象中非 null 的属性值复制到目标对象
*/
class FeixiangBeanCopier {
/**
* 将 src 中非 null 的字段值复制到 dest 的对应字段
* @param src 源对象
* @param dest 目标对象
* @throws Exception 反射异常
*/
public static void copyNonNull(Object src, Object dest) throws Exception {
Class<?> srcClass = src.getClass();
Class<?> destClass = dest.getClass();
// 获取源对象所有字段
Field[] srcFields = srcClass.getDeclaredFields();
for (Field srcField : srcFields) {
srcField.setAccessible(true);
Object value = srcField.get(src);
// 只复制非 null 的值
if (value == null) continue;
try {
// 在目标类中查找同名字段
Field destField = destClass.getDeclaredField(srcField.getName());
destField.setAccessible(true);
// 类型相同才复制
if (destField.getType().equals(srcField.getType())) {
destField.set(dest, value);
}
} catch (NoSuchFieldException e) {
// 目标类没有这个字段,跳过
}
}
}
/**
* 打印对象所有字段和值
*/
public static void printFields(Object obj, String label) throws Exception {
System.out.println("--- " + label + " ---");
Class<?> clazz = obj.getClass();
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
System.out.printf(" %-12s (%s) = %s\n",
f.getName(), f.getType().getSimpleName(), f.get(obj));
}
System.out.println();
}
}
// 源对象类
class EmployeeSource {
private Integer id = 1001;
private String name = "大翔";
private Double salary = 18888.88;
private String department = "研发部";
private String phone = null; // 这个为 null,不会被复制
private String address = "北京朝阳区";
}
// 目标对象类(部分字段不同)
class EmployeeTarget {
private Integer id = 0;
private String name = "未知";
private Double salary = 0.0;
private String department = "默认部门";
private String phone = "13800138000"; // 已有值,不会被覆盖
private String email = "default@feixiang.com"; // 源对象没有,保持不变
}
public class BeanCopyDemo {
public static void main(String[] args) throws Exception {
System.out.println("========== 飞翔科技属性复制演示 ==========\n");
EmployeeSource src = new EmployeeSource();
EmployeeTarget dest = new EmployeeTarget();
System.out.println("【复制前】");
FeixiangBeanCopier.printFields(src, "源对象");
FeixiangBeanCopier.printFields(dest, "目标对象");
// 执行复制
FeixiangBeanCopier.copyNonNull(src, dest);
System.out.println("【复制后】");
FeixiangBeanCopier.printFields(src, "源对象");
FeixiangBeanCopier.printFields(dest, "目标对象(已更新)");
System.out.println("关键点说明:");
System.out.println(" 1. phone 源为 null → 目标保持原值 '13800138000'");
System.out.println(" 2. email 源不存在 → 目标保持原值");
System.out.println(" 3. address 目标没有该字段 → 跳过");
System.out.println(" 4. name/salary/department 被成功复制");
System.out.println("\n========== End ==========");
}
}
运行输出:
========== 飞翔科技属性复制演示 ==========
【复制前】
--- 源对象 ---
id (Integer) = 1001
name (String) = 大翔
salary (Double) = 18888.88
department (String) = 研发部
phone (String) = null
address (String) = 北京朝阳区
--- 目标对象 ---
id (Integer) = 0
name (String) = 未知
salary (Double) = 0.0
department (String) = 默认部门
phone (String) = 13800138000
email (String) = default@feixiang.com
【复制后】
--- 源对象 ---
id (Integer) = 1001
name (String) = 大翔
salary (Double) = 18888.88
department (String) = 研发部
phone (String) = null
address (String) = 北京朝阳区
--- 目标对象(已更新) ---
id (Integer) = 1001
name (String) = 大翔
salary (Double) = 18888.88
department (String) = 研发部
phone (String) = 13800138000
email (String) = default@feixiang.com
关键点说明:
1. phone 源为 null → 目标保持原值 '13800138000'
2. email 源不存在 → 目标保持原值
3. address 目标没有该字段 → 跳过
4. name/salary/department 被成功复制
========== End ==========
示例2:飞翔科技动态调用员工管理系统方法
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* 飞翔科技员工管理系统 —— 通过 Method.invoke() 动态调用
*/
class EmployeeManager {
private Map<Integer, String> employees = new LinkedHashMap<>();
public void addEmployee(int id, String name) {
employees.put(id, name);
System.out.println("[新增] 员工 " + id + " - " + name);
}
public String findEmployee(int id) {
String name = employees.get(id);
if (name != null) {
return "找到员工: " + id + " -> " + name;
}
return "未找到员工 ID: " + id;
}
public void deleteEmployee(int id) {
String removed = employees.remove(id);
if (removed != null) {
System.out.println("[删除] 员工 " + id + " - " + removed);
} else {
System.out.println("[删除] 员工 " + id + " 不存在");
}
}
public List<String> listAllEmployees() {
List<String> result = new ArrayList<>();
for (Map.Entry<Integer, String> entry : employees.entrySet()) {
result.add(entry.getKey() + " -> " + entry.getValue());
}
return result;
}
public double calculateSalary(double base, double bonus) {
return base + bonus;
}
public static void staticMethod(String msg) {
System.out.println("[静态方法] " + msg);
}
}
/**
* 动态方法调度器 —— 通过 Method.invoke() 动态执行
*/
class DynamicDispatcher {
/**
* 根据方法名和参数动态调用
*/
public static Object dispatch(Object target, String methodName, Object... args)
throws Exception {
Class<?> clazz = target.getClass();
// 匹配参数类型
Class<?>[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Integer) {
paramTypes[i] = int.class; // 注意:反射中 Integer → int.class
} else if (arg instanceof Double) {
paramTypes[i] = double.class;
} else {
paramTypes[i] = arg.getClass();
}
}
// 获取 Method 对象
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
// 如果方法是非 public 的,设置可访问
if (!java.lang.reflect.Modifier.isPublic(method.getModifiers())) {
method.setAccessible(true);
}
// 执行调用
long start = System.nanoTime();
Object result = method.invoke(target, args);
long end = System.nanoTime();
System.out.printf(" [反射调用耗时] %s.%s() → %.2f μs\n",
clazz.getSimpleName(), methodName, (end - start) / 1000.0);
return result;
}
}
public class MethodInvokeDemo {
public static void main(String[] args) throws Exception {
System.out.println("========== 飞翔科技动态方法调度系统 ==========\n");
EmployeeManager manager = new EmployeeManager();
DynamicDispatcher dispatcher = new DynamicDispatcher();
// 1. 动态添加员工
System.out.println("【动态操作 1:添加员工】");
dispatcher.dispatch(manager, "addEmployee", 1001, "大翔");
dispatcher.dispatch(manager, "addEmployee", 1002, "白歌");
dispatcher.dispatch(manager, "addEmployee", 1003, "小崔");
dispatcher.dispatch(manager, "addEmployee", 1004, "朱璐");
System.out.println();
// 2. 动态查询员工
System.out.println("【动态操作 2:查询员工】");
Object result1 = dispatcher.dispatch(manager, "findEmployee", 1002);
System.out.println(" 结果: " + result1);
Object result2 = dispatcher.dispatch(manager, "findEmployee", 9999);
System.out.println(" 结果: " + result2);
System.out.println();
// 3. 动态删除员工
System.out.println("【动态操作 3:删除员工】");
dispatcher.dispatch(manager, "deleteEmployee", 1004);
System.out.println();
// 4. 动态列出所有员工
System.out.println("【动态操作 4:列出所有员工】");
Object result3 = dispatcher.dispatch(manager, "listAllEmployees");
System.out.println(" 结果: " + result3);
System.out.println();
// 5. 动态计算薪资(基本类型参数)
System.out.println("【动态操作 5:计算薪资】");
Object result4 = dispatcher.dispatch(manager, "calculateSalary", 15000.0, 3888.88);
System.out.println(" 薪资计算结果: " + result4);
System.out.println();
// 6. 测试找不到方法的情况
System.out.println("【动态操作 6:调用不存在的方法】");
try {
dispatcher.dispatch(manager, "nonExistentMethod");
} catch (NoSuchMethodException e) {
System.out.println(" ✗ 方法不存在: " + e.getMessage());
}
System.out.println("\n========== End ==========");
}
}
运行输出:
========== 飞翔科技动态方法调度系统 ==========
【动态操作 1:添加员工】
[新增] 员工 1001 - 大翔
[反射调用耗时] EmployeeManager.addEmployee() → 125.30 μs
[新增] 员工 1002 - 白歌
[反射调用耗时] EmployeeManager.addEmployee() → 15.20 μs
[新增] 员工 1003 - 小崔
[反射调用耗时] EmployeeManager.addEmployee() → 14.80 μs
[新增] 员工 1004 - 朱璐
[反射调用耗时] EmployeeManager.addEmployee() → 14.50 μs
【动态操作 2:查询员工】
[反射调用耗时] EmployeeManager.findEmployee() → 19.00 μs
结果: 找到员工: 1002 -> 白歌
[反射调用耗时] EmployeeManager.findEmployee() → 12.00 μs
结果: 未找到员工 ID: 9999
【动态操作 3:删除员工】
[删除] 员工 1004 - 朱璐
[反射调用耗时] EmployeeManager.deleteEmployee() → 14.00 μs
【动态操作 4:列出所有员工】
[反射调用耗时] EmployeeManager.listAllEmployees() → 12.50 μs
结果: [1001 -> 大翔, 1002 -> 白歌, 1003 -> 小崔]
【动态操作 5:计算薪资】
[反射调用耗时] EmployeeManager.calculateSalary() → 16.00 μs
薪资计算结果: 18888.88
【动态操作 6:调用不存在的方法】
✗ 方法不存在: ...nonExistentMethod
========== End ==========
八、易错场景
易错1:混淆 getFields() 与 getDeclaredFields()
class Parent {
public int parentPublic = 1;
private int parentPrivate = 2;
}
class Child extends Parent {
public int childPublic = 3;
private int childPrivate = 4;
}
public class ErrorDemo1 {
public static void main(String[] args) {
Class<Child> clazz = Child.class;
// getFields() → 只拿 public,但包含继承的
System.out.println("getFields():");
for (Field f : clazz.getFields()) {
System.out.println(" " + f.getName()); // parentPublic, childPublic
}
// getDeclaredFields() → 拿所有,但不包含继承的
System.out.println("getDeclaredFields():");
for (Field f : clazz.getDeclaredFields()) {
System.out.println(" " + f.getName()); // childPublic, childPrivate
// ← 注意:没有 parentPublic 和 parentPrivate!
}
}
}
关键结论:getDeclaredFields() 只获取当前类声明的字段,不包含从父类继承的。如果要做 ORM 映射,可能需要递归遍历父类。
易错2:invoke 基本类型参数时的自动装箱问题
public class ErrorDemo2 {
// 方法签名使用基本类型
public void setAge(int age) {
System.out.println("Age set to: " + age);
}
public static void main(String[] args) throws Exception {
Method m = ErrorDemo2.class.getDeclaredMethod("setAge", int.class);
// 错误:传 Integer 对象
Integer age = 25;
m.invoke(new ErrorDemo2(), age); // 这可以工作,因为可变参数会自动装箱
// 真正容易出错的是获取 Method 时:
// 错误示范:
// Method m2 = ErrorDemo2.class.getDeclaredMethod("setAge", Integer.class);
// ↑ NoSuchMethodException! 因为反射中 int.class ≠ Integer.class
// 正确的参数类型匹配:
// int → int.class
// long → long.class
// boolean → boolean.class
// double → double.class
}
}
易错3:忽略 InvocationTargetException 的原始异常
public class ErrorDemo3 {
public void riskyMethod() {
throw new RuntimeException("业务异常:余额不足");
}
public static void main(String[] args) {
try {
Method m = ErrorDemo3.class.getDeclaredMethod("riskyMethod");
m.invoke(new ErrorDemo3());
} catch (InvocationTargetException e) {
// 错误做法:只打印 InvocationTargetException
e.printStackTrace();
// 正确做法:获取原始异常
Throwable cause = e.getCause(); // ← RuntimeException: 业务异常:余额不足
System.out.println("真正的异常: " + cause.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
}
易错4:setAccessible 性能陷阱
public class ErrorDemo4 {
private int value = 0;
public static void main(String[] args) throws Exception {
Field field = ErrorDemo4.class.getDeclaredField("value");
ErrorDemo4 obj = new ErrorDemo4();
int iterations = 1_000_000;
// 错误做法:每次迭代都 setAccessible(不要这样!)
long start1 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
field.setAccessible(true); // ← 每次都调用!
field.set(obj, i);
}
long time1 = System.nanoTime() - start1;
// 正确做法:只 setAccessible 一次,缓存 Field 对象
field.setAccessible(true); // ← 只调用一次
long start2 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
field.set(obj, i);
}
long time2 = System.nanoTime() - start2;
System.out.println("每次 setAccessible: " + time1 / 1_000_000 + " ms");
System.out.println("缓存 Field 对象: " + time2 / 1_000_000 + " ms");
// 输出(参考):
// 每次 setAccessible: ~3500 ms
// 缓存 Field 对象: ~15 ms
}
}
九、面试考点
考点1:getFields() 和 getDeclaredFields() 的区别?
答:
getFields():返回当前类和所有父类、接口中的 public 字段。私有字段不返回。getDeclaredFields():返回当前类中声明的所有访问级别的字段(private、protected、public、默认),但不包含从父类继承的字段。- 同名规则也适用于
getMethods()vsgetDeclaredMethods()。 - 要做 ORM 框架需要遍历字段时,通常用
getDeclaredFields()并递归遍历父类super.getDeclaredFields()。
考点2:Method.invoke() 的参数和返回值如何处理基本类型?
答:
- 获取 Method 时:基本类型参数必须精确匹配
int.class,long.class,double.class等,不能用包装类Integer.class代替。 - 调用 invoke 时:传入的参数会自动装箱/拆箱,因为 invoke 接受的是
Object...。 - 返回值:如果方法是
void,invoke 返回null;如果是基本类型返回,会自动装箱(如int→Integer)。 - JDK 8 新增:
Method.getParameterCount()可以方便获取参数个数。
考点3:setAccessible(true) 的作用和潜在问题?
答:
- 作用:绕过 Java 访问控制检查,允许反射访问 private 成员,提高性能(跳过安全检查)。
- 性能提升:setAccessible 后 invoke 会快很多,因为跳过了每次调用的权限检查。
- 安全限制:如果 JVM 配置了 SecurityManager,setAccessible 可能被拒绝。
- JDK 模块化影响:JDK 9+ 引入模块系统后,访问 JDK 内部类需要添加
--add-opens参数。 - 最佳实践:setAccessible 一次,缓存 Field/Method 对象复用,避免重复调用。
考点4:为什么要手动设置 setAccessible(true) 而不始终设为 true?性能影响有多大?
答:
- 安全原因:Java 语言的设计初衷之一是封装,私有成员不应该被随意访问。setAccessible 是一种有意的"越权"操作。
- 性能对比:不设 setAccessible 时,每次 invoke/get/set 都要执行一遍完整的访问控制检查(检查 public/private/protected、检查调用者是否有权限等)。设为 true 后,这些检查被跳过,性能提升 10-50 倍(取决于具体操作)。
- JDK 8 建议:对于会被频繁调用的反射操作,务必缓存 Field/Method 对象并调用 setAccessible(true)。
大翔总结道:"Field 和 Method 是反射体系的两大基石。Field 让你在运行时读写对象状态,Method 让你在运行时调用对象行为。"
白歌补充:"但能力越大,责任越大。setAccessible 破坏了封装,invoke 有性能开销,InvocationTargetException 需要解包——这些都是生产环境中真实踩过的坑。"
朱璐若有所思:"原来 BeanUtils.copyProperties 底层就是 getDeclaredFields + set + get...我现在明白它的性能瓶颈在哪了。"
小崔兴奋地喊道:"我知道了!Spring 的 @Autowired 注入就是用 Field.set() 实现的!"
大翔笑着点头:"没错。下一节课我们就来实战:写一个迷你 IoC 容器。"
附:JDK 8 反射相关新增 API 速查
新增API 说明 Executable.getParameterCount()获取方法/构造器的参数个数 Method.isDefault()判断接口中的默认方法 AnnotatedElement.getAnnotationsByType(Class)支持 @Repeatable 注解读取 Executable.getParameters()获取 Parameter 数组(需编译时 -parameters 开启)