1. 概述

上一篇文章中,我们介绍了如何通过 Java 反射读取其他类中的私有(private)字段值。但实际开发中,我们有时不仅需要读取,还需要修改字段值——比如在某些框架或工具类库中,目标类的字段不可见或无法直接访问。

本文将聚焦于如何使用 Java 的 Reflection API 来设置字段值,包括基本类型和对象类型。✅

提示:本文示例继续沿用上一篇中的 Person 类,结构如下(便于上下文理解):

public class Person {
    private int age;
    private short uidNumber;
    private int pinCode;
    private long contactNumber;
    private float height;
    private double weight;
    private char gender;
    private boolean active;
    private String name;

    // getter 方法省略
}

2. 设置基本类型字段

对于基本类型字段,Java 反射提供了专用的 setXxx() 方法,比通用的 set() 更高效且类型安全。⚠️ 推荐优先使用。

2.1 设置整型字段

可以通过以下方法分别设置 byteshortintlong 类型字段:

  • setByte()
  • setShort()
  • setInt()
  • setLong()
@Test
public void whenSetIntegerFields_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field ageField = person.getClass()
        .getDeclaredField("age");
    ageField.setAccessible(true);

    byte age = 26;
    ageField.setByte(person, age);
    Assertions.assertEquals(age, person.getAge());

    Field uidNumberField = person.getClass()
        .getDeclaredField("uidNumber");
    uidNumberField.setAccessible(true);

    short uidNumber = 5555;
    uidNumberField.setShort(person, uidNumber);
    Assertions.assertEquals(uidNumber, person.getUidNumber());

    Field pinCodeField = person.getClass()
        .getDeclaredField("pinCode");
    pinCodeField.setAccessible(true);

    int pinCode = 411057;
    pinCodeField.setInt(person, pinCode);
    Assertions.assertEquals(pinCode, person.getPinCode());

    Field contactNumberField = person.getClass()
        .getDeclaredField("contactNumber");
    contactNumberField.setAccessible(true);

    long contactNumber = 123456789L;
    contactNumberField.setLong(person, contactNumber);
    Assertions.assertEquals(contactNumber, person.getContactNumber());
}

✅ 支持自动拆箱(Unboxing)

即使传入的是包装类型(如 Integer),setXxx() 方法也能自动拆箱:

@Test
public void whenDoUnboxing_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field pinCodeField = person.getClass()
        .getDeclaredField("pinCode");
    pinCodeField.setAccessible(true);

    Integer pinCode = 411057;
    pinCodeField.setInt(person, pinCode); // 自动拆箱
    Assertions.assertEquals(pinCode, person.getPinCode());
}

✅ 支持窄化转换(Narrowing)

反射允许小范围类型赋值给大范围字段(如 shortint),但反过来不行。下面这个例子是合法的:

@Test
public void whenDoNarrowing_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field pinCodeField = person.getClass()
        .getDeclaredField("pinCode");
    pinCodeField.setAccessible(true);

    short pinCode = 4110;
    pinCodeField.setInt(person, pinCode); // short → int,合法
    Assertions.assertEquals(pinCode, person.getPinCode());
}

❌ 但如果你尝试把 int 值塞进 short 字段,且值超出范围,则会抛异常(见第 4 节)。


2.2 设置浮点型字段

对应 floatdouble 字段,使用:

  • setFloat()
  • setDouble()
@Test
public void whenSetFloatingTypeFields_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field heightField = person.getClass()
        .getDeclaredField("height");
    heightField.setAccessible(true);

    float height = 6.1242f;
    heightField.setFloat(person, height);
    Assertions.assertEquals(height, person.getHeight());

    Field weightField = person.getClass()
        .getDeclaredField("weight");
    weightField.setAccessible(true);

    double weight = 75.2564;
    weightField.setDouble(person, weight);
    Assertions.assertEquals(weight, person.getWeight());
}

2.3 设置字符型字段

使用 setChar() 设置 char 类型字段:

@Test
public void whenSetCharacterFields_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field genderField = person.getClass()
        .getDeclaredField("gender");
    genderField.setAccessible(true);

    char gender = 'M';
    genderField.setChar(person, gender);
    Assertions.assertEquals(gender, person.getGender());
}

2.4 设置布尔型字段

使用 setBoolean() 设置 boolean 字段:

@Test
public void whenSetBooleanFields_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field activeField = person.getClass()
        .getDeclaredField("active");
    activeField.setAccessible(true);

    activeField.setBoolean(person, true);
    Assertions.assertTrue(person.isActive());
}

简单粗暴,没啥可说的。


3. 设置对象类型字段

对于引用类型(如 String、自定义对象等),统一使用通用的 Field.set(Object obj, Object value) 方法:

@Test
public void whenSetObjectFields_thenSuccess() 
  throws Exception {
    Person person = new Person();

    Field nameField = person.getClass()
        .getDeclaredField("name");
    nameField.setAccessible(true);

    String name = "Umang Budhwar";
    nameField.set(person, name);
    Assertions.assertEquals(name, person.getName());
}

⚠️ 注意:这里传入的 value 必须与字段声明类型兼容,否则会抛 IllegalArgumentException


4. 常见异常处理

反射操作容易“翻车”,下面列出两个最常踩坑的异常。

4.1 IllegalArgumentException

传入的值类型与字段类型不兼容时抛出。典型场景:

  • setInt()String 字段赋值
  • 窄化转换失败(如 long 值太大,无法转为 int

示例:尝试给 String 字段用 setInt() 赋值 ❌

@Test
public void givenInt_whenSetStringField_thenIllegalArgumentException() 
  throws Exception {
    Person person = new Person();
    Field nameField = person.getClass()
        .getDeclaredField("name");
    nameField.setAccessible(true);

    Assertions.assertThrows(IllegalArgumentException.class, () -> nameField.setInt(person, 26));
}

另一个坑:虽然支持窄化,但如果值超出目标字段范围,也会失败。比如下面这个 long 值其实可以转成 int,但如果 pinCodeFieldshort 类型且值超限,就会炸。

但注意:本例中 pinCodeint,而传入的是 long,JVM 不允许自动窄化(防止精度丢失),所以也会抛异常:

@Test
public void givenInt_whenSetLongField_thenIllegalArgumentException() 
  throws Exception {
    Person person = new Person();

    Field pinCodeField = person.getClass()
        .getDeclaredField("pinCode");
    pinCodeField.setAccessible(true);

    long pinCode = 411057L;

    Assertions.assertThrows(IllegalArgumentException.class, () -> pinCodeField.setLong(person, pinCode));
}

✅ 正确做法是先手动转类型:pinCodeField.setInt(person, (int) pinCode);


4.2 IllegalAccessException

如果你没调用 setAccessible(true) 就去访问私有字段,JVM 会直接拒绝并抛出此异常。

这是新手最容易忽略的点,尤其在单元测试里容易忘记模拟权限开放。

@Test
public void whenFieldNotSetAccessible_thenIllegalAccessException() 
  throws Exception {
    Person person = new Person();
    Field nameField = person.getClass()
        .getDeclaredField("name");
    // 忘记调用 setAccessible(true) ❌

    Assertions.assertThrows(IllegalAccessException.class, () -> nameField.set(person, "Umang Budhwar"));
}

✅ 踩坑提示:所有 getDeclaredField() 拿到的私有成员,必须 setAccessible(true) 才能读写,否则权限不足。


5. 总结

本文系统讲解了如何通过 Java 反射设置各类字段值,涵盖:

  • 基本类型:使用专用 setXxx() 方法更安全高效
  • 对象类型:统一用 set() 方法
  • 支持自动拆箱和窄化转换(但需小心溢出)
  • 两个关键异常:IllegalArgumentException(类型不匹配)、IllegalAccessException(权限不足)

反射虽强大,但也容易带来性能损耗和安全风险,建议仅在框架、序列化、测试工具等必要场景使用。生产代码中滥用反射,后期维护会哭的 😅

完整示例代码已托管至 GitHub:https://github.com/dev-example/tutorials/tree/master/core-java-modules/core-java-reflection-2


原始标题:Set Field Value With Reflection