Java记录类入门:简化的以数据为中心的Java编程

内容目录

记录类声明是一种在Java类中封装数据同时减少样板代码的高效方式。本文将通过基础及高级编程场景介绍其工作原理。

文件柜中的文件记录 图片来源:Stokkete / Shutterstock*

Java记录类是一种用于存储数据的新型类。无需编写构造方法、访问器、equals()hashCode()toString() 的样板代码,只需声明字段,Java编译器便会自动处理其余部分。本文将通过基础与高级用例示例,以及不适用记录类的场景,带您全面了解Java记录类。

注意:Java记录类在JDK 16中正式定型。

Java编译器如何处理记录类

传统Java创建简单数据类需要大量样板代码。以下通过Java吉祥物Duke和Juggy的示例说明:

public class JavaMascot {
    private final String name;
    private final int yearCreated;

    public JavaMascot(String name, int yearCreated) {
        this.name = name;
        this.yearCreated = yearCreated;
    }

    public String getName() { return name; }
    public int getYearCreated() { return yearCreated; }

    // 为简洁起见,省略equals、hashCode和toString方法
}

使用记录类后,上述代码可简化为单行:

public record JavaMascot(String name, int yearCreated) {}

这一简洁声明自动提供了私有final字段、构造方法、访问器方法,以及正确实现的 equals()hashCode()toString() 方法。

定义记录类后,即可投入使用:

public class RecordExample {
    public static void main(String[] args) {
        JavaMascot duke = new JavaMascot("Duke", 1996);
        JavaMascot juggy1 = new JavaMascot("Juggy", 2005);
        JavaMascot juggy2 = new JavaMascot("Juggy", 2005);

        System.out.println(duke); // 输出:JavaMascot[name=Duke, yearCreated=1996]
        System.out.println(juggy1.equals(juggy2)); // 输出:true
        System.out.println(duke.equals(juggy1));   // 输出:false
        System.out.println("吉祥物名称:" + duke.name());
        System.out.println("创建年份:" + duke.yearCreated());
    }
}

记录类自动提供有意义的字符串表示、基于值的等值比较,以及与组件名称匹配的简单访问器方法。

自定义记录类

虽然记录类设计简洁,但仍可通过自定义行为增强功能。以下是相关示例。

紧凑型构造方法

记录类提供特殊的“紧凑型构造方法”语法,无需重复参数列表即可验证或转换输入参数:

record JavaMascot(String name, int yearCreated) {
    // 带验证的紧凑型构造方法
    public JavaMascot {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("名称不能为空");
        }
        if (yearCreated < 1995) {
            throw new IllegalArgumentException("Java吉祥物在1995年前不存在");
        }
    }
}

紧凑型构造方法在字段初始化后、对象完全构建前运行,非常适合用于参数验证。此示例中省略了参数声明,但这些参数在构造方法内仍隐式可用。

添加方法

我们还可以为记录类添加方法:

record JavaMascot(String name, int yearCreated) {
    public boolean isOriginalMascot() {
        return name.equals("Duke");
    }

    public int yearsActive() {
        return java.time.Year.now().getValue() - yearCreated;
    }
}

通过添加方法,记录类可在保持语法简洁和不可变性的同时,封装与其数据相关的行为。

接下来,我们探讨记录类更高级的用法。

使用 instanceofswitch 进行模式匹配

Java 21中,记录类成为模式匹配的关键部分,支持switch表达式、组件解构、嵌套模式和守卫条件。

结合增强的 instanceof 运算符,记录类可在类型验证时简洁地提取组件:

record Person(String name, int age) {}

if (obj instanceof Person person) {
    System.out.println("姓名:" + person.name());
}

再看一个经典示例。几何形状是展示密封接口如何与记录类协同工作的典型例子,这种组合使模式匹配尤为清晰。Switch表达式(Java 17引入)的优雅性在此凸显,它让代码简洁且类型安全,类似于函数式语言中的代数数据类型:

sealed interface Shape permits Rectangle, Circle, Triangle {}

record Rectangle(double width, double height) implements Shape {}
record Circle(double radius) implements Shape {}
record Triangle(double base, double height) implements Shape {}

public class RecordPatternMatchingExample {
    public static void main(String[] args) {
        Shape shape = new Circle(5);

        // 表达性强且类型安全的模式匹配
        double area = switch (shape) {
            case Rectangle r -> r.width() * r.height();
            case Circle c    -> Math.PI * c.radius() * c.radius();
            case Triangle t  -> t.base() * t.height() / 2;
        };

        System.out.println("面积 = " + area);
    }
}

此例中,Shape 是密封接口,仅允许 RectangleCircleTriangle 实现。由于类型集合封闭,switch表达式覆盖所有情况,无需 default 分支。

Java中的模式匹配

若想进一步探索记录类与模式匹配,请参阅我的近期教程:《Java基础与高级模式匹配》

将记录类用作数据传输对象

记录类在现代API设计(如REST、GraphQL、gRPC或服务间通信)中作为数据传输对象(DTO)表现卓越。其简洁语法和内置等值比较特性,使其成为服务层间映射的理想选择。例如:

record UserDTO(String username, String email, Set<String> roles) {}
record OrderDTO(UUID id, UserDTO user, List<ProductDTO> items, BigDecimal total) {}

DTO在微服务应用中无处不在。使用记录类可使DTO更健壮(得益于不可变性),更简洁(无需编写构造方法、getter及 equals()hashCode() 等方法)

函数式与并发编程中的记录类

作为不可变数据容器,记录类完美契合函数式与并发编程需求。它们既可作为纯函数的返回类型,也可用于流处理管道,还能安全地在线程间共享数据。

由于字段为final且不可变,记录类避免了一整类线程问题。一旦构建完成,其状态无法更改,因此无需防御性复制或同步即可实现线程安全。参考以下示例:

transactions.parallelStream().mapToDouble(Transaction::amount).sum();

由于记录类不可变,此并行计算天生具备线程安全性。

不适用Java记录类的场景

至此,我们已了解记录类的优势,但它们并非万能替代品。例如,所有记录类隐式继承 java.lang.Record,因此无法继承其他类(但可实现接口)。在需要类继承的场景中,记录类并不适用。

以下是记录类不适用的其他情况。

记录类设计为不可变

记录类组件始终为final,因此不适用于需要可变/有状态对象的场景。以下示例展示了一个依赖状态变化的可变类,而记录类不允许此类操作:

public class GameCharacter {
    private int health;
    private Position position;

    public void takeDamage(int amount) {
        this.health = Math.max(0, this.health - amount);
    }

    public void move(int x, int y) {
        this.position = new Position(this.position.x() + x, this.position.y() + y);
    }
}

记录类不适合复杂行为建模

基于可变状态、复杂业务逻辑或策略模式、访问者模式、观察者模式等设计,更适合使用传统类实现。以下是复杂逻辑不适用于记录类的示例:

public class TaxCalculator {
    private final TaxRateProvider rateProvider;
    private final DeductionRegistry deductions;

    public TaxAssessment calculateTax(Income income, Residence residence) {
        // 复杂逻辑不适用于记录类
    }
}

记录类与某些框架不兼容

部分框架(尤其是ORM)可能无法良好支持记录类。序列化或重度依赖反射的工具也可能存在问题。请务必检查Java特性与技术栈的兼容性:

// 可能无法与某些ORM框架良好协作
record Employee(Long id, String name, Department department) {}

// 此时仍需使用传统实体类
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    private Department department;

    // Getter、setter、equals、hashCode等方法
}

这些注意事项并不意味着记录类功能不完整,而是强调记录类专为特定场景设计。在某些情况下,传统类仍是更实用的选择。

Java中的记录类与序列化

记录类已在Java生态中被广泛采用,其不可变性使其在持久化、配置和数据传输中极具吸引力。记录类可像普通类一样实现 Serializable 接口。可序列化的记录类组件天然适用于保存配置、恢复状态、网络传输数据或缓存值等场景。

由于记录类字段为final且不可变,它们有助于避免可变状态在序列化与反序列化之间发生变化引发的问题。例如:

import java.io.Serializable;

record User(String username, int age, Profile profile) implements Serializable {}

class Profile {
    private String bio;
}

此例中,Stringint 可序列化,但 Profile 不可序列化,因此 User 无法序列化。若将 Profile 也改为实现 Serializable,则 User 将完全可序列化:

class Profile implements Serializable {
    private String bio;
}

除序列化基础外,Java生态对记录类的支持已迅速成熟。Spring Boot、Quarkus和Jackson等流行框架均与记录类无缝协作,大多数测试工具也是如此。

得益于这种广泛采纳,记录类在实际API中作为DTO表现卓越:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public OrderView getOrder(@PathVariable UUID id) {
        // 实际应用中,此数据应来自数据库或服务
        return new OrderView(
            id,
            "Duke",
            List.of(new ItemView(UUID.randomUUID(), 2)),
            new BigDecimal("149.99")
        );
    }

    // 用于API响应的记录类DTO
    record OrderView(UUID id, String customerName, List<ItemView> items, BigDecimal total) {}
    record ItemView(UUID productId, int quantity) {}
}

如今,大多数主流Java库和工具已将记录类视为一等公民。早期的质疑已基本消散,开发者正因其清晰性与安全性而广泛接纳记录类。

结语

记录类是Java演进过程中的重大进步。它们降低了数据类的冗余度,并确保了不可变性和行为一致性。通过消除构造方法、访问器及 equals()hashCode() 等方法的样板代码,记录类使代码更简洁、表达力更强,在保持类型安全的同时契合现代实践。

记录类并非适用于所有场景,但在处理不可变数据时优势显著。结合模式匹配,它们能让代码意图更清晰,同时由Java编译器处理样板代码。

随着记录类、密封类和模式匹配等技术的进步,Java正稳步迈向更以数据为中心的编程风格。掌握这些工具是编写现代、高表达力Java代码的最清晰路径之一。


【注】本文译自:Introduction to Java records: Simplified data-centric programming in Java

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注