Field 成员变量反射 | JavaSE
Field 成员变量反射
一、学习目标
学完本章,你应该能够:
- 解释
Field在 Java 反射体系中代表什么。 - 区分
getFields()、getDeclaredFields()、getField()、getDeclaredField()。 - 获取字段的名称、类型以及基本修饰信息。
- 使用
Field.get()动态读取对象字段。 - 使用
Field.set()动态修改对象字段。 - 理解普通实例字段与
static字段在反射操作中的区别。 - 理解
setAccessible(true)与 Java 21 访问控制、模块边界之间的关系。 - 理解反射操作字段为什么能够支持对象映射、序列化、ORM 等框架技术。
二、核心知识
2.1 从普通成员变量到 Field
假设有一个 Dog 类:
public class Dog {
private String name;
private int age;
private String hobby;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
在普通 Java 编程中,我们看到的是:
Dog
│
├── name
├── age
└── hobby
这些叫:
成员变量(Member Variable)。
在 Java 反射体系中,一个字段会被描述成:
java.lang.reflect.Field
因此:
private String hobby;
↓
Field 对象
也就是说:
Field是 Java 反射 API 对类或接口中某个字段的运行时表示。
拿到 Field 以后,程序不仅可以知道:
这个字段叫什么
是什么类型
由哪个类声明
有什么修饰符
还可以在符合访问规则的情况下:
读取字段值
修改字段值
2.2 回顾整个反射知识模型
目前已经学习:
Class
│
├── Constructor
│ ↓
│ 创建对象
│
└── Field
↓
操作数据
下一章还会继续:
Class
│
└── Method
↓
调用方法
最终得到:
Class
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Constructor Field Method
↓ ↓ ↓
创建对象 操作数据 执行行为
这套模型非常重要。
2.3 获取 Field 的核心目的
获取字段通常有两类目的。
第一类:
分析字段
例如:
字段名称是什么?
字段类型是什么?
是不是 private?
是不是 static?
这个类到底定义了哪些数据?
第二类:
操作字段
即:
读取字段的值
修改字段的值
对应两个最核心的方法:
field.get(object);
field.set(object, value);
可以简单记成:
Field
├── get → 取值
└── set → 赋值
三、使用方法
3.1 准备测试类
package com.example.reflect;
public class Dog {
public String category = "犬科";
private String name;
private int age;
private String hobby;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"category='" + category + '\'' +
", name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}
现在 Dog 声明了:
public String category
private String name
private int age
private String hobby
3.2 获取所有 public 字段
使用:
getFields()
例如:
Class<Dog> clazz = Dog.class;
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
getFields() 获取:
当前类或接口可以访问到的
public字段,包括符合规则的继承 public 字段。
因此它并不是简单的:
当前类自己声明的 public 字段
如果父类存在:
public String id;
子类调用:
Child.class.getFields();
也可能获取到这个继承的 public 字段。
3.3 获取当前类声明的所有字段
使用:
getDeclaredFields()
例如:
Field[] fields =
Dog.class.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
它关注:
当前类自己声明了哪些字段。
因此包括:
public
protected
默认访问权限
private
但不负责把父类声明的字段全部包含进来。
可以建立:
| API | 范围 |
| --------------------- | ------------------------ |
| getFields() | public 字段,并考虑继承 |
| getDeclaredFields() | 当前类自己声明的所有字段 |
这里再次强调:
Declared
≈ 当前类声明的
不是简单理解成:
Declared = private
3.4 获取指定 public 字段
假设:
public String category;
可以:
Field field =
Dog.class.getField("category");
这里使用:
字段名称
来定位字段。
3.5 获取指定声明字段
例如:
private String hobby;
使用:
Field field =
Dog.class.getDeclaredField("hobby");
如果使用:
Dog.class.getField("hobby");
就无法因为它是 private 而正常找到。
所以四个 API 可以形成统一模型:
Field
│
┌───────────────┴───────────────┐
↓ ↓
多个 单个
│ │
┌─────┴─────┐ ┌─────┴─────┐
↓ ↓ ↓ ↓
public declared public declared
getFields() getField(name)
getDeclaredFields() getDeclaredField(name)
3.6 NoSuchFieldException
假设类中只有:
private String hobby;
却执行:
Dog.class.getDeclaredField("hobbies");
字段名字写错了。
可能产生:
NoSuchFieldException
反射高度依赖:
运行时提供的信息必须准确。
这也是反射相对于普通 Java 编程的一个重要特点。
普通:
dog.hobby
很多错误编译器能够提前发现。
反射:
getDeclaredField(fieldName);
fieldName 可能来自配置文件。
编译器并不知道:
这个字符串到底对不对。
错误往往延迟到运行期。
3.7 获取字段名称
Field field =
Dog.class.getDeclaredField("hobby");
System.out.println(field.getName());
得到:
hobby
3.8 获取字段类型
Class<?> type = field.getType();
System.out.println(type.getName());
对于:
private String hobby;
得到:
java.lang.String
对于:
private int age;
则:
field.getType()
对应:
int.class
因此可以:
for (Field field :
Dog.class.getDeclaredFields()) {
System.out.println(
field.getName()
+ " : "
+ field.getType().getName()
);
}
可能输出:
category : java.lang.String
name : java.lang.String
age : int
hobby : java.lang.String
这已经可以做到:
完全不知道
Dog具体定义了什么字段,却能在运行时全部扫描出来。
3.9 获取字段修饰符
可以:
int modifiers = field.getModifiers();
返回的是一个整数形式的修饰符信息。
通常配合:
java.lang.reflect.Modifier
使用。
例如:
System.out.println(
Modifier.toString(
field.getModifiers()
)
);
如果字段:
private String hobby;
可能得到:
private
还可以判断:
Modifier.isPrivate(
field.getModifiers()
);
Modifier.isStatic(
field.getModifiers()
);
Modifier.isFinal(
field.getModifiers()
);
以后框架就可以根据字段类型选择不同策略。
3.10 使用 Field.get() 读取字段
先测试 public 字段。
Dog dog = new Dog("泰迪", 3);
Field field =
Dog.class.getField("category");
Object value = field.get(dog);
System.out.println(value);
如果:
category = "犬科";
结果:
犬科
核心:
field.get(dog);
可以理解成:
从 dog 对象中
读取 field 所描述的那个字段。
3.11 为什么 get() 返回 Object
方法:
Object get(Object obj)
必须能够适用于:
String
Integer
Dog
User
double
boolean
……
各种字段。
所以统一返回:
Object
例如:
Object value = field.get(dog);
如果确定字段是 String:
String hobby =
(String) field.get(dog);
3.12 使用 Field.set() 修改字段
例如 public 字段:
Field field =
Dog.class.getField("category");
Dog dog = new Dog("泰迪", 3);
field.set(dog, "宠物犬");
可以把它理解成普通语义:
dog.category = "宠物犬";
反射版本:
field.set(dog, "宠物犬");
其统一格式:
field.set(
要修改哪个对象,
新值
);
3.13 private 字段的访问问题
现在获取:
private String hobby;
Field field =
Dog.class.getDeclaredField("hobby");
注意:
能获取到 Field,不代表一定拥有读取或修改该字段的权限。
如果直接:
field.set(dog, "社交");
在权限不允许的情况下可能出现:
IllegalAccessException
3.14 setAccessible(true)
在允许开放反射访问的环境中,可以:
field.setAccessible(true);
然后:
Dog dog = new Dog("泰迪", 3);
field.set(dog, "社交");
Object value = field.get(dog);
完整:
Class<Dog> clazz = Dog.class;
Field field =
clazz.getDeclaredField("hobby");
field.setAccessible(true);
Dog dog = new Dog("泰迪", 3);
field.set(dog, "社交");
String hobby =
(String) field.get(dog);
System.out.println(hobby);
可以把:
field.set(dog, "社交");
类比为普通代码中的:
dog.setHobby("社交");
而:
field.get(dog);
类似于:
dog.getHobby();
但反射:
不依赖我们提前知道具体 getter / setter。
3.15 读取多个字段
现在真正开始体现反射的通用性。
public static void printFields(
Object object
) throws IllegalAccessException {
Class<?> clazz = object.getClass();
Field[] fields =
clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
Object value = field.get(object);
System.out.println(
name + "=" + value
);
}
}
调用:
Dog dog = new Dog("小黑", 3);
printFields(dog);
关键是:
printFields(Object object)
没有写:
Dog
所以以后:
printFields(student);
printFields(teacher);
printFields(user);
理论上都可以执行相同逻辑。
这就是反射通用框架思想的入口。
3.16 static 字段
假设:
public class Dog {
public static String SPECIES = "Canis";
}
static 字段属于:
类
而不是某一个对象。
普通访问:
Dog.SPECIES
反射仍然可以:
Field field =
Dog.class.getField("SPECIES");
Object value = field.get(null);
或者修改:
field.set(null, "Canis familiaris");
因为对于静态字段:
具体对象实例不是核心目标。
但实际工程代码中:
不要因为反射“能改”就随意改变全局静态状态。
四、原理与进阶
4.1 getFields 与 getDeclaredFields 最容易混淆的点
有:
class Animal {
public String species;
private String secret;
}
class Dog extends Animal {
private String name;
}
对于:
Dog.class.getFields();
重点是:
public + 成员规则 + 继承
所以:
species
可能被获取。
而:
secret
不会因为它是父类 private 字段而出现。
对于:
Dog.class.getDeclaredFields();
关注:
Dog 自己声明了什么
所以得到:
name
但不会自动把父类 species、secret 全部带下来。
因此:
getFields
≠ 当前类的 public 字段
getDeclaredFields
≠ 当前类加所有父类的全部字段
这两个边界必须真正理解。
4.2 如何遍历整个继承体系的字段
如果框架确实希望:
当前类
+
父类
+
祖先类
都检查,就可以自己沿继承链遍历。
基本思想:
Class<?> current =
object.getClass();
while (current != null) {
Field[] fields =
current.getDeclaredFields();
// 处理字段
current =
current.getSuperclass();
}
这说明:
getDeclaredFields()只负责当前一层。
是否继续向父类走,是框架自己的设计决策。
4.3 setAccessible(true) 不是“private 万能破解器”
原课程为了方便理解使用:
暴力反射
这个说法有教学价值。
但在 Java 21 中,更准确地说:
field.setAccessible(true);
是在:
请求抑制 Java 语言访问控制检查。
现代 Java 还存在模块系统(JPMS)。
如果目标类型位于另外一个模块,并且对应包没有向调用模块开放,那么:
setAccessible(true)
可能失败并产生:
InaccessibleObjectException
所以正确理解应该是:
getDeclaredField()
↓
拿到字段描述对象
↓
访问权限可能不允许
↓
尝试开放反射访问
↓
是否成功还受模块边界约束
4.4 final 字段需要特别谨慎
例如:
private final String id;
你可能产生一个想法:
既然 setAccessible 能访问 private,
那 final 应该也可以随便改?
不能这样理解。
Java 21 对一些 final 字段存在明确的不可修改限制,例如:
static final 字段
record 的 final 字段
hidden class 中的 final 字段
即便反射访问被开放:
也不能把
setAccessible(true)当成任意写 final 字段的保证。
工程上更重要的原则是:
反射主要用于读取结构和按照明确框架协议操作对象,而不是为了破坏对象不变量。
4.5 反射为什么会削弱编译期类型检查
普通:
dog.setAge("三岁");
编译器立刻知道:
String
不能赋给
int
但是反射:
field.set(object, value);
参数类型都是非常通用的运行时对象。
很多问题只能执行到这里以后才发现:
目标字段是不是 int?
value 到底是什么?
对象是不是目标类型?
因此反射提供:
动态性 ↑
同时意味着:
部分编译期安全 ↓
运行期错误风险 ↑
代码理解难度 ↑
所以反射不是普通业务代码的默认替代方案。
五、实践应用
5.1 通用对象打印器
假设:
public static void printObject(
Object object
) throws IllegalAccessException {
Class<?> clazz =
object.getClass();
System.out.println(
"==== "
+ clazz.getSimpleName()
+ " ===="
);
for (Field field :
clazz.getDeclaredFields()) {
field.setAccessible(true);
System.out.println(
field.getName()
+ "="
+ field.get(object)
);
}
}
它可以处理:
Dog
Student
Teacher
User
Product
因为程序没有写死:
dog.getName();
student.getAge();
teacher.getSalary();
而是:
运行时扫描字段。
5.2 对象序列化思想
假设:
Student student =
new Student("小明", 18);
框架希望写成:
name=小明
age=18
它完全可以:
获取全部 Field
↓
读取字段名称
↓
读取字段值
↓
输出文本
这正是原始课程下一章:
SaveObjectFrameWork
案例的核心思想。
5.3 JSON 对象转换
以后使用:
Jackson
Gson
Fastjson
时会遇到:
String json =
mapper.writeValueAsString(user);
你只传入:
user
框架却能知道:
有哪些字段?
叫什么名字?
是什么类型?
字段值是什么?
这里就涉及类似:
运行时类型分析
的机制。
实际成熟框架会比我们现在的示例复杂得多,但反射思想是理解它们的重要基础。
5.4 ORM 对象映射
例如数据库返回:
id = 1
username = "LingXi"
age = 21
Java 对象:
User user = ...
框架需要解决:
数据库列
↓
Java 属性/字段
↓
对象
因此反射经常与:
ORM
对象映射
Bean 属性
注解
配合工作。
这一部分以后学习 MyBatis、Spring 时会再次出现。
六、常见问题
6.1 Field 就是成员变量本身吗?
准确来说:
Field是某个字段的反射表示对象。
例如:
private String hobby;
是代码中的字段声明。
通过:
getDeclaredField("hobby")
得到的是:
Field
对象。
6.2 getField 和 getDeclaredField 的核心区别是什么?
简单记:
getField
→ public 成员查找规则
getDeclaredField
→ 当前类声明的字段,不限制访问修饰符
6.3 getDeclaredFields 会得到父类 private 字段吗?
不会自动把整个父类字段体系全部返回。
它关注:
当前类自身声明的字段。
如果需要父类字段,需要继续获取父类 Class 后再扫描。
6.4 Field.get 为什么需要传 object?
因为:
private String name;
是实例字段。
可能同时存在:
Dog dog1 = ...
Dog dog2 = ...
同一个:
Field nameField
需要知道:
到底读取 dog1 的 name,还是 dog2 的 name?
所以:
field.get(dog1);
field.get(dog2);
6.5 Field.set 两个参数分别是什么?
field.set(object, value);
分别代表:
object
→ 修改哪个对象
value
→ 修改成什么值
6.6 private 字段是不是只要 getDeclaredField 就能修改?
不是。
获取 Field
与:
拥有访问权限
是两回事。
6.7 setAccessible(true) 是否一定成功?
Java 21 中:
不一定。
模块边界可能阻止深度反射访问。
6.8 能不能随便修改 final 字段?
不应该这样理解。
尤其现代 Java 对部分不可修改 final 字段存在明确限制。
工程上也不应该把:
绕过对象设计约束
当作反射的常规使用方式。
七、练习与验收
7.1 知识问答
Field表示什么?getFields()与getDeclaredFields()有什么区别?getField()与getDeclaredField()有什么区别?Field.get()的作用是什么?Field.set()的作用是什么?- 为什么
Field.get()要接收目标对象? - 如何获取字段名称和字段类型?
- 为什么
getDeclaredFields()不等于“当前类及所有父类的全部字段”? - 为什么反射可能把部分错误从编译期推迟到运行期?
- Java 21 中为什么不能把
setAccessible(true)理解成万能破解器?
7.2 代码阅读
class Animal {
public String species;
private int secret;
}
class Dog extends Animal {
private String name;
public int age;
}
分析:
Field[] a =
Dog.class.getFields();
Field[] b =
Dog.class.getDeclaredFields();
回答:
- 哪一个会重点包含继承的 public 字段?
- 哪一个只关注
Dog自己声明的字段? - 父类
secret会不会自动出现在getDeclaredFields()中? - 如何继续获得父类声明字段?
7.3 手写代码
定义:
public class Student {
private String name;
private int age;
private double score;
}
要求:
编写:
public static void printFields(
Object object
)
输出:
name : java.lang.String
age : int
score : double
要求:
- 不硬编码任何字段名。
- 必须使用反射遍历。
7.4 Debug
代码:
Field field =
Student.class
.getDeclaredField("age");
Student student = new Student();
field.set(student, "十八");
分析:
- 问题发生在哪一行?
age的字段类型是什么?"十八"的运行时类型是什么?- 为什么普通 Java 赋值更容易提前发现这种错误?
- 如何正确修改?
7.5 综合训练
编写一个通用方法:
public static void printObject(
Object object
)
使:
Student student = ...
printObject(student);
可以输出类似:
====== Student ======
name=小明
age=18
score=95.5
再传入:
Dog dog = ...
仍然可以工作。
禁止:
if (object instanceof Student)
if (object instanceof Dog)
等针对业务类的特殊判断。
7.6 本章验收
能够闭卷手写:
Class<?> clazz = object.getClass();
Field[] fields =
clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String name =
field.getName();
Class<?> type =
field.getType();
Object value =
field.get(object);
}
并准确解释:
Field 是什么
getFields 与 getDeclaredFields
getField 与 getDeclaredField
get 与 set
访问控制
继承字段边界
即可认为掌握本章主干。