1. 概述

Java Records 是 Java 14 中引入的一种简洁定义不可变数据容器的方式。

本文将探讨 Java Records 中的自定义构造器如何通过数据验证和错误处理,让我们在对象初始化时获得更强的控制力。

2. 理解 Java Records

Records 提供简洁可读的语法,强制不可变性,并自动生成常用方法的标准实现,如 toString()hashCode()equals()。这些实现基于记录的组件(components),由编译器自动生成。

使用 record 关键字定义记录,后跟记录名称和其组件:

record StudentRecord(String name, int rollNo, int marks) {}

此定义创建了一个名为 StudentRecord 的记录类,包含三个组件:namerollNomarks

这些组件是浅不可变实例变量,意味着记录实例创建后无法修改。但记录中包含的可变对象本身仍可被修改。

默认情况下,记录组件是私有的,只能通过访问器方法访问。可向记录添加自定义方法和行为,但组件必须保持 private 且仅通过访问器访问:

@Test
public void givenStudentRecordData_whenCreated_thenStudentPropertiesMatch() {
     StudentRecord s1 = new StudentRecord("John", 1, 90);
     StudentRecord s2 = new StudentRecord("Jane", 2, 80);

     assertEquals("John", s1.name());
     assertEquals(1, s1.rollNo());
     assertEquals(90, s1.marks());
     assertEquals("Jane", s2.name());
     assertEquals(2, s2.rollNo());
     assertEquals(80, s2.marks());
}

本例中,我们使用生成的访问器方法验证记录组件的值。

3. 如何为 Java Record 创建自定义构造器

自定义构造器在 Java 记录中至关重要,它们允许添加额外逻辑并控制记录对象的创建。

与 Java 编译器提供的标准实现相比,自定义构造器为记录提供了更多功能。

为确保数据完整性并支持按名称排序 StudentRecord,我们可以创建一个用于输入验证和字段初始化的自定义构造器:

record Student(String name, int rollNo, int marks) {
    public Student {
        if (name == null) {
            throw new IllegalArgumentException("Name cannot be null");
        }
    }
}

这里,自定义构造器检查 name 组件是否为 null。如果是,则抛出 IllegalArgumentException。这使我们能验证输入数据,确保记录对象在有效状态下创建。

3.1. 排序 StudentRecord 对象列表

了解了如何创建自定义构造器后,我们用它来按名称排序 StudentRecord 列表:

@Test
public void givenStudentRecordsList_whenSortingDataWithName_thenStudentsSorted(){
    List<StudentRecord> studentRecords = new ArrayList<>(List.of(
      new StudentRecord("Dana", 1, 85),
      new StudentRecord("Jim", 2, 90),
      new StudentRecord("Jane", 3, 80)
    ));

    studentRecords.sort(Comparator.comparing(StudentRecord::name));
    assertEquals("Jane", studentRecords.get(1).name());
}

本例中,我们创建了一个 StudentRecord 列表并按 name 排序。由于 name 永不为 null,排序时无需处理 null 值

总之,Java 记录中的自定义构造器允许我们添加额外逻辑并控制记录对象的创建。虽然标准实现简单直接,但自定义构造器让记录更灵活实用。

4. Java Records 自定义构造器的优势与限制

如同任何语言特性,Java Records 的自定义构造器也有其优缺点。下面我们详细探讨。

4.1. 额外验证

自定义构造器能为 Java Records 带来诸多好处,例如对传入数据提供额外验证,如检查值是否在特定范围内或满足特定条件。

假设我们需确保 StudentRecordmarks 字段始终在 0 到 100 之间。可创建自定义构造器检查范围,若超出则抛出异常或设为默认值 0:

public StudentRecord {
    if (marks < 0 || marks > 100) {
        throw new IllegalArgumentException("Marks should be between 0 and 100.");
    }
}

接下来创建一个 marks 无效的 StudentRecord

assertThrows(IllegalArgumentException.class, () -> new StudentRecord("Jane", 2, 150));

测试显示,我们得到了预期的 IllegalArgumentException

4.2. 执行额外计算

Java 记录的自定义构造器还能用于提取和聚合相关数据到更少的组件中,简化记录数据处理。

例如,我们想根据分数计算学生等级。可向 StudentRecord 添加 grade 字段,并创建自定义构造器基于 marks 计算等级。这样无需每次手动计算即可直接访问等级:

record StudentRecordV2(String name, int rollNo, int marks, char grade) {
    public StudentRecordV2(String name, int rollNo, int marks) {
        this(name, rollNo, marks, calculateGrade(marks));
    }

    private static char calculateGrade(int marks) {
        if (marks >= 90) {
            return 'A';
        } else if (marks >= 80) {
            return 'B';
        } else if (marks >= 70) {
            return 'C';
        } else if (marks >= 60) {
            return 'D';
        } else {
            return 'F';
        }
    }
}

快速测试一下:

StudentRecordV2 studentV2 = new StudentRecordV2("Jane", 2, 85);
assertEquals('B', studentV2.grade());

可见 studentV2.grade() 属性被正确计算。

4.3. 属性默认值

此外,自定义构造器可在未提供参数时为其设置默认值。这在需要为某些字段提供默认值或自动生成值时很有用。示例如下:

record StudentRecordV3(String id, String name, Set<String> hobbies, boolean active) {
  
    public StudentRecordV3(String name) {
        this(UUID.randomUUID().toString(), name, new HashSet<>(), true);
    }
}

StudentRecordV3 记录定义了 String (id)Set (hobbies)Boolean (active) 属性。我们创建了一个仅接受 name 的自定义构造器,并为其他属性提供默认值。

这在需要使用默认值创建实例时很方便:

StudentRecordV3 studentV3 = new StudentRecordV3("Jane");
  
assertThat(studentV3.id()).isEmpty();
assertThat(studentV3.hobbies()).isEmpty();
assertTrue(studentV3.active());

有时我们需要一个不可变默认实例的单个对象。此时可考虑使用 static 变量:

record UserPreference(Map<String, String> preferences, boolean superUser) {
    public static final UserPreference DEFAULT = new UserPreference(
      Map.of("language", "EN", "timezone", "UTC"), false
    );
}

UserPreference 记录包含 MapBoolean 参数。我们还声明了 DEFAULT 静态变量,用于包含不可变 Mapboolean 值的默认 UserPreference 实例:

UserPreference defaultOne = UserPreference.DEFAULT;
assertFalse(defaultOne.superUser());
assertEquals(Map.of("language", "EN", "timezone", "UTC"), defaultOne.preferences());
 
UserPreference defaultTwo = UserPreference.DEFAULT;
assertSame(defaultOne, defaultTwo);

测试显示,通过 UserPreference.DEFAULT 获取的默认实例始终是同一个对象

4.4. 限制

尽管自定义构造器为 Java Records 带来诸多好处,但也存在限制。

⚠️ 重载记录构造器必须在第一行显式委托给另一个记录构造器。此要求存在是因为所有构造最终必须委托给规范构造器(canonical constructor)。任何重载构造器必须在第一行使用 this(...) 委托给另一个构造器(来源:java-record-canonical-constructor)。

例如,以下实现无效,因为未在第一行调用记录构造器

record BadStudentRecord(String name, int rollNo, int marks, String id) {

    public BadStudentRecord(String name, int rollNo, int marks) {
        name = name.toUpperCase();
        this(name, rollNo, marks, UUID.randomUUID().toString());
    }
}

5. 总结

本文介绍了 Java Records 中的自定义构造器及其优缺点。

✅ 总结:Java Records 和自定义构造器简化了代码,提升了可读性和可维护性。它们优势明显,但使用限制使其不适用于所有场景。