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

但不会自动把父类 speciessecret 全部带下来。

因此:

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 知识问答

  1. Field 表示什么?
  2. getFields()getDeclaredFields() 有什么区别?
  3. getField()getDeclaredField() 有什么区别?
  4. Field.get() 的作用是什么?
  5. Field.set() 的作用是什么?
  6. 为什么 Field.get() 要接收目标对象?
  7. 如何获取字段名称和字段类型?
  8. 为什么 getDeclaredFields() 不等于“当前类及所有父类的全部字段”?
  9. 为什么反射可能把部分错误从编译期推迟到运行期?
  10. 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();

回答:

  1. 哪一个会重点包含继承的 public 字段?
  2. 哪一个只关注 Dog 自己声明的字段?
  3. 父类 secret 会不会自动出现在 getDeclaredFields() 中?
  4. 如何继续获得父类声明字段?

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, "十八");

分析:

  1. 问题发生在哪一行?
  2. age 的字段类型是什么?
  3. "十八" 的运行时类型是什么?
  4. 为什么普通 Java 赋值更容易提前发现这种错误?
  5. 如何正确修改?

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
访问控制
继承字段边界

即可认为掌握本章主干。