1. 概述

自Java 14以来,record已被引入来表示不可变的数据。记录类包含各种类型的字段,有时我们需要在程序中提取这些字段及其对应的值。

在这个教程中,我们将探讨如何使用Java的反射API在记录类中获取所有字段及其值。

2. 问题介绍

一个例子可以快速解释这个问题。假设我们有一个Player记录:

record Player(String name, int age, Long score) {}

如上所示,Player记录有三个不同类型的字段:String、基本类型intLong。此外,我们创建了一个Player实例:

Player ERIC = new Player("Eric", 28, 4242L);

现在,我们将查找Player记录中声明的所有字段,并使用程序提取ERIC实例的相应值。

为了简化,我们将使用单元测试断言来验证每种方法是否产生预期结果。

接下来,让我们深入研究。

3. 使用RecordComponent

我们提到record是在Java 14中引入的。与record一起,java.lang.reflect包中新增了一个成员:RecordComponent。这是Java 14和15的预览功能,但在Java 16中它升级为永久特性。RecordComponent类提供了关于记录类组件的信息。

此外,Class类提供了getRecordComponents()方法,如果Class对象是Record实例,它将返回记录类的所有组件。值得注意的是,返回数组中的组件按记录中声明的顺序排列。

现在,让我们看看如何使用RecordComponent和反射API从我们的Player记录类获取所有字段以及ERIC实例的对应值:

var fields = new ArrayList<Field>();
RecordComponent[] components = Player.class.getRecordComponents();
for (var comp : components) {
    try {
        Field field = ERIC.getClass()
          .getDeclaredField(comp.getName());
        field.setAccessible(true);
        fields.add(field);
    } catch (NoSuchFieldException e) {
        // for simplicity, error handling is skipped
    }
}

首先,我们创建一个空的字段列表,用于存储稍后提取的字段。我们知道,我们可以使用class.getDeclaredField(fieldName)方法在Java类中检索声明的字段。因此,fieldName成为了解决问题的关键。

RecordComponent携带有关记录字段的各种信息,包括类型、名称等。也就是说,如果我们有PlayerRecordComponent对象,我们可以得到它的Field对象。Player.class.getRecordComponents()返回Player记录类的所有组件作为数组。因此,我们可以根据components数组中的名称获取所有Field对象。

由于我们稍后需要提取这些字段的值,所以在添加到结果字段列表之前,必须为每个字段设置setAccessible(true)

接下来,让我们验证从上述循环中获取的字段是否符合预期:

assertEquals(3, fields.size());

var nameField = fields.get(0); 
var ageField = fields.get(1);
var scoreField = fields.get(2);
try {
    assertEquals("name", nameField.getName());
    assertEquals(String.class, nameField.getType());
    assertEquals("Eric", nameField.get(ERIC));

    assertEquals("age", ageField.getName());
    assertEquals(int.class, ageField.getType());
    assertEquals(28, ageField.get(ERIC));

    assertEquals("score", scoreField.getName());
    assertEquals(Long.class, scoreField.getType());
    assertEquals(4242L, scoreField.get(ERIC));
} catch (IllegalAccessException exception) {
    // for simplicity, error handling is skipped
}

正如断言代码所示,我们可以通过调用field.get(ERIC)获取ERIC实例的值。另外,在调用此方法时,我们必须捕获IllegalAccessException(检查异常)

4. 使用Class.getDeclaredFields()

新的RecordComponent使我们能够轻松获取记录组件的属性。然而,不使用新RecordComponent类也可以解决这个问题。

Java反射API提供了Class.getDeclaredFields()方法来获取类中声明的所有字段。因此,我们可以使用此方法获取记录类的字段。

值得注意的是,我们不应该使用Class.getFields()方法获取记录类的字段。这是因为getFields()只返回类中声明的公共字段。然而,记录类中的所有字段都是私有的。因此,如果我们对记录类调用Class.getFields(),我们将不会得到任何字段:

// record has no public fields
assertEquals(0, Player.class.getFields().length);

同样,在将字段添加到结果列表之前,我们对每个字段应用setAccessible(true)

var fields = new ArrayList<Field>();
for (var field : Player.class.getDeclaredFields()) {
    field.setAccessible(true);
    fields.add(field);
}

接下来,让我们检查结果列表中的字段是否与Player类匹配,以及我们能否通过这些字段获取ERIC对象的预期值:

assertEquals(3, fields.size());
var nameField = fields.get(0);
var ageField = fields.get(1);
var scoreField = fields.get(2);

try {
    assertEquals("name", nameField.getName());
    assertEquals(String.class, nameField.getType());
    assertEquals("Eric", nameField.get(ERIC));

    assertEquals("age", ageField.getName());
    assertEquals(int.class, ageField.getType());
    assertEquals(28, ageField.get(ERIC));
 
    assertEquals("score", scoreField.getName());
    assertEquals(Long.class, scoreField.getType());
    assertEquals(4242L, scoreField.get(ERIC));
} catch (IllegalAccessException ex) {
    // for simplicity, error handling is skipped
}

当运行测试时,它通过了。所以,这个方法也解决了问题。

5. 总结

在这篇文章中,我们探讨了两种使用反射从记录类中提取字段的方法。

在第一个解决方案中,我们从RecordComponent类中获取字段的名称。然后,我们可以通过调用Class.getDeclaredField(Field_Name)来获取字段对象。

记录类也是一个Java类。因此,另一种选择是,我们也可以从Class.getDeclaredFields()方法中获取记录类的所有字段。

如往常一样,示例的完整源代码可以在GitHub上找到这里