1. 概述
自Java 14以来,record
已被引入来表示不可变的数据。记录类包含各种类型的字段,有时我们需要在程序中提取这些字段及其对应的值。
在这个教程中,我们将探讨如何使用Java的反射API在记录类中获取所有字段及其值。
2. 问题介绍
一个例子可以快速解释这个问题。假设我们有一个Player
记录:
record Player(String name, int age, Long score) {}
如上所示,Player
记录有三个不同类型的字段:String
、基本类型int
和Long
。此外,我们创建了一个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
携带有关记录字段的各种信息,包括类型、名称等。也就是说,如果我们有Player
的RecordComponent
对象,我们可以得到它的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上找到这里。